diff --git a/lib/consts.dart b/lib/consts.dart index 85e2d4aa..51915ef0 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -157,6 +157,12 @@ const kTextButtonMinWidth = 44.0; const kRandMax = 100000; +const kSuggestionsMenuWidth = 300.0; +const kSuggestionsMenuMaxHeight = 200.0; + +const kReqResTabWidth = 280.0; +const kReqResTabHeight = 32.0; + const kDataTableScrollbarTheme = ScrollbarThemeData( crossAxisMargin: -4, ); diff --git a/lib/providers/environment_provider.dart b/lib/providers/environment_providers.dart similarity index 89% rename from lib/providers/environment_provider.dart rename to lib/providers/environment_providers.dart index 72566e22..8a117632 100644 --- a/lib/providers/environment_provider.dart +++ b/lib/providers/environment_providers.dart @@ -15,6 +15,26 @@ final selectedEnvironmentModelProvider = return selectedId != null ? environments![selectedId] : null; }); +final availableEnvironmentVariablesStateProvider = + StateProvider>>((ref) { + Map> result = {}; + final environments = ref.watch(environmentsStateNotifierProvider); + final activeEnviormentId = ref.watch(activeEnvironmentIdStateProvider); + if (activeEnviormentId != null) { + result[activeEnviormentId] = environments?[activeEnviormentId] + ?.values + .where((element) => element.enabled) + .toList() ?? + []; + } + result[kGlobalEnvironmentId] = environments?[kGlobalEnvironmentId] + ?.values + .where((element) => element.enabled) + .toList() ?? + []; + return result; +}); + final StateNotifierProvider?> environmentsStateNotifierProvider = StateNotifierProvider((ref) => EnvironmentsStateNotifier(ref, hiveHandler)); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 726afaa2..32f808ae 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -1,4 +1,4 @@ export 'collection_providers.dart'; -export 'environment_provider.dart'; +export 'environment_providers.dart'; export 'settings_providers.dart'; export 'ui_providers.dart'; diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index 69ec4f1a..0fff037b 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/models.dart'; -import 'environment_provider.dart'; import '../services/services.dart' show hiveHandler, HiveHandler; import '../consts.dart'; @@ -11,26 +10,6 @@ final codegenLanguageStateProvider = StateProvider((ref) => final activeEnvironmentIdStateProvider = StateProvider((ref) => ref.watch(settingsProvider.select((value) => value.activeEnvironmentId))); -final availableEnvironmentVariablesStateProvider = - StateProvider>>((ref) { - Map> result = {}; - final environments = ref.watch(environmentsStateNotifierProvider); - final activeEnviormentId = ref.watch(activeEnvironmentIdStateProvider); - if (activeEnviormentId != null) { - result[activeEnviormentId] = environments?[activeEnviormentId] - ?.values - .where((element) => element.enabled) - .toList() ?? - []; - } - result[kGlobalEnvironmentId] = environments?[kGlobalEnvironmentId] - ?.values - .where((element) => element.enabled) - .toList() ?? - []; - return result; -}); - final StateNotifierProvider settingsProvider = StateNotifierProvider((ref) => ThemeStateNotifier(hiveHandler)); diff --git a/lib/screens/common/environment_autocomplete.dart b/lib/screens/common/environment_autocomplete.dart deleted file mode 100644 index b4eb8d24..00000000 --- a/lib/screens/common/environment_autocomplete.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_portal/flutter_portal.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:apidash/consts.dart'; -import 'package:apidash/models/environment_model.dart'; -import 'package:apidash/providers/providers.dart'; -import 'package:apidash/utils/envvar_utils.dart'; -import 'package:apidash/widgets/widgets.dart'; - -class EnvironmentAutocompleteField extends HookConsumerWidget { - const EnvironmentAutocompleteField({ - super.key, - required this.keyId, - this.initialValue, - this.onChanged, - this.onFieldSubmitted, - this.style, - this.decoration, - }); - - final String keyId; - final String? initialValue; - final void Function(String)? onChanged; - final void Function(String)? onFieldSubmitted; - final TextStyle? style; - final InputDecoration? decoration; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mentionValue = ref.watch(environmentFieldEditStateProvider); - final envMap = ref.watch(availableEnvironmentVariablesStateProvider); - final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider); - final initialMentions = - getMentions(initialValue, envMap, activeEnvironmentId); - final suggestions = getEnvironmentVariableSuggestions( - mentionValue, envMap, activeEnvironmentId); - return EnvironmentAutocompleteFieldBase( - key: Key(keyId), - mentionValue: mentionValue, - onMentionValueChanged: (value) { - ref.read(environmentFieldEditStateProvider.notifier).state = value; - }, - initialValue: initialValue, - initialMentions: initialMentions, - suggestions: suggestions, - onChanged: onChanged, - onFieldSubmitted: onFieldSubmitted, - style: style, - decoration: decoration, - ); - } -} - -class EnvironmentVariableSpan extends HookConsumerWidget { - const EnvironmentVariableSpan({ - super.key, - required this.suggestion, - }); - - final EnvironmentVariableSuggestion suggestion; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final environments = ref.watch(environmentsStateNotifierProvider); - final envMap = ref.watch(availableEnvironmentVariablesStateProvider); - final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider); - - final currentSuggestion = - getCurrentVariableStatus(suggestion, envMap, activeEnvironmentId); - - final showPopover = useState(false); - - final isMissingVariable = currentSuggestion.isUnknown; - final String scope = isMissingVariable - ? 'unknown' - : getEnvironmentTitle( - environments?[currentSuggestion.environmentId]?.name); - final colorScheme = Theme.of(context).colorScheme; - - var text = Text( - '{{${currentSuggestion.variable.key}}}', - style: TextStyle( - color: isMissingVariable ? colorScheme.error : colorScheme.primary, - fontWeight: FontWeight.w600), - ); - - return PortalTarget( - visible: showPopover.value, - portalFollower: MouseRegion( - onEnter: (_) { - showPopover.value = true; - }, - onExit: (_) { - showPopover.value = false; - }, - child: - EnvironmentPopoverCard(suggestion: currentSuggestion, scope: scope), - ), - anchor: const Aligned( - follower: Alignment.bottomCenter, - target: Alignment.topCenter, - backup: Aligned( - follower: Alignment.topCenter, - target: Alignment.bottomCenter, - ), - ), - child: kIsMobile - ? TapRegion( - onTapInside: (_) { - showPopover.value = true; - }, - onTapOutside: (_) { - showPopover.value = false; - }, - child: text, - ) - : MouseRegion( - onEnter: (_) { - showPopover.value = true; - }, - onExit: (_) { - showPopover.value = false; - }, - child: text, - ), - ); - } -} - -class EnvironmentPopoverCard extends StatelessWidget { - const EnvironmentPopoverCard({ - super.key, - required this.suggestion, - required this.scope, - }); - - final EnvironmentVariableSuggestion suggestion; - final String scope; - - @override - Widget build(BuildContext context) { - return Material( - type: MaterialType.card, - borderRadius: kBorderRadius8, - elevation: 8, - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 200), - child: Ink( - padding: kP8, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: kBorderRadius8, - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - EnvironmentIndicator(suggestion: suggestion), - kHSpacer10, - Text(suggestion.variable.key), - ], - ), - kVSpacer5, - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'VALUE', - style: Theme.of(context).textTheme.labelSmall, - ), - kHSpacer10, - Text(suggestion.variable.value), - ], - ), - kVSpacer5, - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'SCOPE', - style: Theme.of(context).textTheme.labelSmall, - ), - kHSpacer10, - Text(scope), - ], - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/screens/common/main_editor_widgets.dart b/lib/screens/common/main_editor_widgets.dart deleted file mode 100644 index 7c3cd249..00000000 --- a/lib/screens/common/main_editor_widgets.dart +++ /dev/null @@ -1,218 +0,0 @@ -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 ScaffoldTitle extends StatelessWidget { - const ScaffoldTitle({ - super.key, - required this.title, - this.showMenu = true, - this.onSelected, - }); - final String title; - final bool showMenu; - final Function(ItemMenuOption)? onSelected; - - @override - Widget build(BuildContext context) { - return IgnorePointer( - ignoring: !showMenu, - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Material( - color: Colors.transparent, - child: ItemCardMenu( - offset: const Offset(0, 40), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - splashRadius: 0, - tooltip: title, - onSelected: onSelected, - child: Ink( - color: Theme.of(context) - .colorScheme - .secondaryContainer - .withOpacity(0.3), - padding: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6), - child: Row( - children: [ - Expanded( - child: Text( - title, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, - maxLines: 1, - ), - ), - showMenu - ? const Icon( - Icons.more_vert_rounded, - size: 20, - ) - : const SizedBox.shrink(), - ], - ), - ), - ), - ), - ), - ); - } -} - -class EnvironmentDropdown extends ConsumerWidget { - const EnvironmentDropdown({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final environments = ref.watch(environmentsStateNotifierProvider); - final environmentsList = environments?.values.toList(); - environmentsList - ?.removeWhere((element) => element.id == kGlobalEnvironmentId); - final activeEnvironment = ref.watch(activeEnvironmentIdStateProvider); - return Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - ), - borderRadius: kBorderRadius8, - ), - child: EnvironmentPopupMenu( - activeEnvironment: environments?[activeEnvironment], - environments: environmentsList, - onChanged: (value) { - ref.read(activeEnvironmentIdStateProvider.notifier).state = - value?.id; - ref - .read(settingsProvider.notifier) - .update(activeEnvironmentId: value?.id); - ref.read(hasUnsavedChangesProvider.notifier).state = true; - }, - )); - } -} - -class TitleActionsArray extends StatelessWidget { - const TitleActionsArray({ - super.key, - this.onRenamePressed, - this.onDuplicatePressed, - this.onDeletePressed, - }); - - final void Function()? onRenamePressed; - final void Function()? onDuplicatePressed; - final void Function()? onDeletePressed; - - @override - Widget build(BuildContext context) { - var verticalDivider = VerticalDivider( - width: 2, - color: Theme.of(context).colorScheme.outlineVariant, - ); - return ClipRRect( - borderRadius: kBorderRadius20, - child: Material( - color: Colors.transparent, - child: Ink( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - ), - borderRadius: kBorderRadius20, - ), - child: SizedBox( - height: 32, - child: IntrinsicHeight( - child: Row( - children: [ - iconButton( - "Rename", - Icons.edit_rounded, - onRenamePressed, - padding: const EdgeInsets.only(left: 4), - ), - verticalDivider, - iconButton( - "Delete", - Icons.delete_rounded, - onDeletePressed, - ), - verticalDivider, - iconButton( - "Duplicate", - Icons.copy_rounded, - onDuplicatePressed, - padding: const EdgeInsets.only(right: 4), - ), - ], - ), - ), - ), - ), - ), - ); - } - - Widget iconButton( - String tooltip, IconData iconData, void Function()? onPressed, - {EdgeInsets padding = const EdgeInsets.all(0)}) { - return Tooltip( - message: tooltip, - child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(const EdgeInsets.all(0) + padding), - shape: MaterialStateProperty.all(const ContinuousRectangleBorder()), - ), - onPressed: onPressed, - icon: Icon( - iconData, - size: 16, - ), - ), - ); - } -} - -showRenameDialog( - BuildContext context, - String dialogTitle, - String? name, - Function(String) onRename, -) { - showDialog( - context: context, - builder: (context) { - final controller = TextEditingController(text: name ?? ""); - controller.selection = - TextSelection(baseOffset: 0, extentOffset: controller.text.length); - return AlertDialog( - title: Text(dialogTitle), - content: TextField( - autofocus: true, - controller: controller, - decoration: const InputDecoration(hintText: "Enter new name"), - ), - actions: [ - OutlinedButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('CANCEL')), - FilledButton( - onPressed: () { - final val = controller.text.trim(); - onRename(val); - Navigator.pop(context); - Future.delayed(const Duration(milliseconds: 100), () { - controller.dispose(); - }); - }, - child: const Text('OK')), - ], - ); - }); -} diff --git a/lib/screens/common/sidebar_widgets.dart b/lib/screens/common/sidebar_widgets.dart deleted file mode 100644 index 33753f13..00000000 --- a/lib/screens/common/sidebar_widgets.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:apidash/providers/providers.dart'; -import 'package:apidash/extensions/extensions.dart'; -import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/consts.dart'; - -class SidebarHeader extends ConsumerWidget { - const SidebarHeader({super.key, this.onAddNew}); - final Function()? onAddNew; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mobileScaffoldKey = ref.read(mobileScaffoldKeyStateProvider); - return Padding( - padding: kPe8, - child: Row( - children: [ - const SaveButton(), - const Spacer(), - ElevatedButton( - onPressed: onAddNew, - child: const Text( - kLabelPlusNew, - style: kTextStyleButton, - ), - ), - context.width <= kMinWindowSize.width - ? IconButton( - style: IconButton.styleFrom( - padding: const EdgeInsets.all(4), - minimumSize: const Size(30, 30)), - onPressed: () { - mobileScaffoldKey.currentState?.closeDrawer(); - }, - icon: const Icon(Icons.chevron_left), - ) - : const SizedBox.shrink(), - ], - ), - ); - } -} - -class SidebarFilter extends StatelessWidget { - const SidebarFilter({ - super.key, - this.onFilterFieldChanged, - this.filterHintText, - }); - - final Function(String)? onFilterFieldChanged; - final String? filterHintText; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(right: 8), - decoration: BoxDecoration( - borderRadius: kBorderRadius8, - border: Border.all( - color: Theme.of(context).colorScheme.surfaceVariant, - ), - ), - child: Row( - children: [ - kHSpacer5, - Icon( - Icons.filter_alt, - size: 18, - color: Theme.of(context).colorScheme.secondary, - ), - kHSpacer5, - Expanded( - child: RawTextField( - style: Theme.of(context).textTheme.bodyMedium, - hintText: filterHintText ?? "Filter by name", - onChanged: onFilterFieldChanged, - ), - ), - ], - ), - ); - } -} - -class SaveButton extends ConsumerWidget { - const SaveButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final overlayWidget = OverlayWidgetTemplate(context: context); - final savingData = ref.watch(saveDataStateProvider); - final hasUnsavedChanges = ref.watch(hasUnsavedChangesProvider); - return TextButton.icon( - onPressed: (savingData || !hasUnsavedChanges) - ? null - : () async { - overlayWidget.show( - widget: const SavingOverlay(saveCompleted: false)); - - await ref - .read(collectionStateNotifierProvider.notifier) - .saveData(); - await ref - .read(environmentsStateNotifierProvider.notifier) - .saveEnvironments(); - overlayWidget.hide(); - overlayWidget.show( - widget: const SavingOverlay(saveCompleted: true)); - await Future.delayed(const Duration(seconds: 1)); - overlayWidget.hide(); - }, - icon: const Icon( - Icons.save, - size: 20, - ), - label: const Text( - kLabelSave, - style: kTextStyleButton, - ), - ); - } -} diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart new file mode 100644 index 00000000..57282325 --- /dev/null +++ b/lib/screens/common_widgets/common_widgets.dart @@ -0,0 +1,11 @@ +export 'editor_title.dart'; +export 'editor_title_actions.dart'; +export 'envfield_url.dart'; +export 'envfield_cell.dart'; +export 'environment_dropdown.dart'; +export 'envvar_indicator.dart'; +export 'envvar_span.dart'; +export 'envvar_popover.dart'; +export 'sidebar_filter.dart'; +export 'sidebar_header.dart'; +export 'sidebar_save_button.dart'; diff --git a/lib/screens/common_widgets/editor_title.dart b/lib/screens/common_widgets/editor_title.dart new file mode 100644 index 00000000..453e2a4f --- /dev/null +++ b/lib/screens/common_widgets/editor_title.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; + +class EditorTitle extends StatelessWidget { + const EditorTitle({ + super.key, + required this.title, + this.showMenu = true, + this.onSelected, + }); + final String title; + final bool showMenu; + final Function(ItemMenuOption)? onSelected; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: !showMenu, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Material( + color: Colors.transparent, + child: ItemCardMenu( + offset: const Offset(0, 40), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + splashRadius: 0, + tooltip: title, + onSelected: onSelected, + child: Ink( + color: Theme.of(context) + .colorScheme + .secondaryContainer + .withOpacity(0.3), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6), + child: Row( + children: [ + Expanded( + child: Text( + title, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + maxLines: 1, + ), + ), + showMenu + ? const Icon( + Icons.more_vert_rounded, + size: 20, + ) + : const SizedBox.shrink(), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/common_widgets/editor_title_actions.dart b/lib/screens/common_widgets/editor_title_actions.dart new file mode 100644 index 00000000..a142a345 --- /dev/null +++ b/lib/screens/common_widgets/editor_title_actions.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class EditorTitleActions extends StatelessWidget { + const EditorTitleActions({ + super.key, + this.onRenamePressed, + this.onDuplicatePressed, + this.onDeletePressed, + }); + + final void Function()? onRenamePressed; + final void Function()? onDuplicatePressed; + final void Function()? onDeletePressed; + + @override + Widget build(BuildContext context) { + var verticalDivider = VerticalDivider( + width: 2, + color: Theme.of(context).colorScheme.outlineVariant, + ); + return ClipRRect( + borderRadius: kBorderRadius20, + child: Material( + color: Colors.transparent, + child: Ink( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + borderRadius: kBorderRadius20, + ), + child: SizedBox( + height: 32, + child: IntrinsicHeight( + child: Row( + children: [ + iconButton( + "Rename", + Icons.edit_rounded, + onRenamePressed, + padding: const EdgeInsets.only(left: 4), + ), + verticalDivider, + iconButton( + "Delete", + Icons.delete_rounded, + onDeletePressed, + ), + verticalDivider, + iconButton( + "Duplicate", + Icons.copy_rounded, + onDuplicatePressed, + padding: const EdgeInsets.only(right: 4), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget iconButton( + String tooltip, IconData iconData, void Function()? onPressed, + {EdgeInsets padding = const EdgeInsets.all(0)}) { + return Tooltip( + message: tooltip, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(const EdgeInsets.all(0) + padding), + shape: MaterialStateProperty.all(const ContinuousRectangleBorder()), + ), + onPressed: onPressed, + icon: Icon( + iconData, + size: 16, + ), + ), + ); + } +} diff --git a/lib/screens/common/environment_textfields.dart b/lib/screens/common_widgets/envfield_cell.dart similarity index 58% rename from lib/screens/common/environment_textfields.dart rename to lib/screens/common_widgets/envfield_cell.dart index ad44a3b9..532c9379 100644 --- a/lib/screens/common/environment_textfields.dart +++ b/lib/screens/common_widgets/envfield_cell.dart @@ -1,41 +1,6 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; -import 'environment_autocomplete.dart'; - -class EnvURLField extends StatelessWidget { - const EnvURLField({ - super.key, - required this.selectedId, - this.initialValue, - this.onChanged, - this.onFieldSubmitted, - }); - - final String selectedId; - final String? initialValue; - final void Function(String)? onChanged; - final void Function(String)? onFieldSubmitted; - - @override - Widget build(BuildContext context) { - return EnvironmentAutocompleteField( - keyId: "url-$selectedId", - initialValue: initialValue, - style: kCodeStyle, - decoration: InputDecoration( - hintText: kHintTextUrlCard, - hintStyle: kCodeStyle.copyWith( - color: Theme.of(context).colorScheme.outline.withOpacity( - kHintOpacity, - ), - ), - border: InputBorder.none, - ), - onChanged: onChanged, - onFieldSubmitted: onFieldSubmitted, - ); - } -} +import 'environment_field.dart'; class EnvCellField extends StatelessWidget { const EnvCellField({ @@ -56,7 +21,7 @@ class EnvCellField extends StatelessWidget { @override Widget build(BuildContext context) { var clrScheme = colorScheme ?? Theme.of(context).colorScheme; - return EnvironmentAutocompleteField( + return EnvironmentField( keyId: keyId, initialValue: initialValue, style: kCodeStyle.copyWith( diff --git a/lib/screens/common_widgets/envfield_url.dart b/lib/screens/common_widgets/envfield_url.dart new file mode 100644 index 00000000..d4982f5b --- /dev/null +++ b/lib/screens/common_widgets/envfield_url.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'environment_field.dart'; + +class EnvURLField extends StatelessWidget { + const EnvURLField({ + super.key, + required this.selectedId, + this.initialValue, + this.onChanged, + this.onFieldSubmitted, + }); + + final String selectedId; + final String? initialValue; + final void Function(String)? onChanged; + final void Function(String)? onFieldSubmitted; + + @override + Widget build(BuildContext context) { + return EnvironmentField( + keyId: "url-$selectedId", + initialValue: initialValue, + style: kCodeStyle, + decoration: InputDecoration( + hintText: kHintTextUrlCard, + hintStyle: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.outline.withOpacity( + kHintOpacity, + ), + ), + border: InputBorder.none, + ), + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + ); + } +} diff --git a/lib/screens/common_widgets/environment_dropdown.dart b/lib/screens/common_widgets/environment_dropdown.dart new file mode 100644 index 00000000..db012b32 --- /dev/null +++ b/lib/screens/common_widgets/environment_dropdown.dart @@ -0,0 +1,37 @@ +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 EnvironmentDropdown extends ConsumerWidget { + const EnvironmentDropdown({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final environments = ref.watch(environmentsStateNotifierProvider); + final environmentsList = environments?.values.toList(); + environmentsList + ?.removeWhere((element) => element.id == kGlobalEnvironmentId); + final activeEnvironment = ref.watch(activeEnvironmentIdStateProvider); + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + borderRadius: kBorderRadius8, + ), + child: EnvironmentPopupMenu( + activeEnvironment: environments?[activeEnvironment], + environments: environmentsList, + onChanged: (value) { + ref.read(activeEnvironmentIdStateProvider.notifier).state = + value?.id; + ref + .read(settingsProvider.notifier) + .update(activeEnvironmentId: value?.id); + ref.read(hasUnsavedChangesProvider.notifier).state = true; + }, + )); + } +} diff --git a/lib/screens/common_widgets/environment_field.dart b/lib/screens/common_widgets/environment_field.dart new file mode 100644 index 00000000..21188030 --- /dev/null +++ b/lib/screens/common_widgets/environment_field.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/utils/envvar_utils.dart'; +import 'package:apidash/widgets/widgets.dart'; + +class EnvironmentField extends HookConsumerWidget { + const EnvironmentField({ + super.key, + required this.keyId, + this.initialValue, + this.onChanged, + this.onFieldSubmitted, + this.style, + this.decoration, + }); + + final String keyId; + final String? initialValue; + final void Function(String)? onChanged; + final void Function(String)? onFieldSubmitted; + final TextStyle? style; + final InputDecoration? decoration; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mentionValue = ref.watch(environmentFieldEditStateProvider); + final envMap = ref.watch(availableEnvironmentVariablesStateProvider); + final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider); + final initialMentions = + getMentions(initialValue, envMap, activeEnvironmentId); + final suggestions = getEnvironmentVariableSuggestions( + mentionValue, envMap, activeEnvironmentId); + return EnvironmentFieldBase( + key: Key(keyId), + mentionValue: mentionValue, + onMentionValueChanged: (value) { + ref.read(environmentFieldEditStateProvider.notifier).state = value; + }, + initialValue: initialValue, + initialMentions: initialMentions, + suggestions: suggestions, + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + style: style, + decoration: decoration, + ); + } +} diff --git a/lib/screens/common_widgets/envvar_indicator.dart b/lib/screens/common_widgets/envvar_indicator.dart new file mode 100644 index 00000000..9f1e861d --- /dev/null +++ b/lib/screens/common_widgets/envvar_indicator.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +class EnvVarIndicator extends StatelessWidget { + const EnvVarIndicator({super.key, required this.suggestion}); + + final EnvironmentVariableSuggestion suggestion; + + @override + Widget build(BuildContext context) { + final isUnknown = suggestion.isUnknown; + final isGlobal = suggestion.environmentId == kGlobalEnvironmentId; + return Container( + padding: kP4, + decoration: BoxDecoration( + color: isUnknown + ? Theme.of(context).colorScheme.errorContainer + : isGlobal + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.primaryContainer, + borderRadius: kBorderRadius4, + ), + child: Icon( + isUnknown + ? Icons.block + : isGlobal + ? Icons.public + : Icons.computer, + size: 16, + ), + ); + } +} diff --git a/lib/screens/common_widgets/envvar_popover.dart b/lib/screens/common_widgets/envvar_popover.dart new file mode 100644 index 00000000..2c017889 --- /dev/null +++ b/lib/screens/common_widgets/envvar_popover.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; +import 'common_widgets.dart'; + +class EnvVarPopover extends StatelessWidget { + const EnvVarPopover({ + super.key, + required this.suggestion, + required this.scope, + }); + + final EnvironmentVariableSuggestion suggestion; + final String scope; + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.card, + borderRadius: kBorderRadius8, + elevation: 8, + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 200), + child: Ink( + padding: kP8, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: kBorderRadius8, + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + EnvVarIndicator(suggestion: suggestion), + kHSpacer10, + Text(suggestion.variable.key), + ], + ), + kVSpacer5, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'VALUE', + style: Theme.of(context).textTheme.labelSmall, + ), + kHSpacer10, + Text(suggestion.variable.value), + ], + ), + kVSpacer5, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'SCOPE', + style: Theme.of(context).textTheme.labelSmall, + ), + kHSpacer10, + Text(scope), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/common_widgets/envvar_span.dart b/lib/screens/common_widgets/envvar_span.dart new file mode 100644 index 00000000..5c698db2 --- /dev/null +++ b/lib/screens/common_widgets/envvar_span.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/utils/utils.dart'; +import 'envvar_popover.dart'; + +class EnvVarSpan extends HookConsumerWidget { + const EnvVarSpan({ + super.key, + required this.suggestion, + }); + + final EnvironmentVariableSuggestion suggestion; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final environments = ref.watch(environmentsStateNotifierProvider); + final envMap = ref.watch(availableEnvironmentVariablesStateProvider); + final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider); + + final currentSuggestion = + getCurrentVariableStatus(suggestion, envMap, activeEnvironmentId); + + final showPopover = useState(false); + + final isMissingVariable = currentSuggestion.isUnknown; + final String scope = isMissingVariable + ? 'unknown' + : getEnvironmentTitle( + environments?[currentSuggestion.environmentId]?.name); + final colorScheme = Theme.of(context).colorScheme; + + var text = Text( + '{{${currentSuggestion.variable.key}}}', + style: TextStyle( + color: isMissingVariable ? colorScheme.error : colorScheme.primary, + fontWeight: FontWeight.w600), + ); + + return PortalTarget( + visible: showPopover.value, + portalFollower: MouseRegion( + onEnter: (_) { + showPopover.value = true; + }, + onExit: (_) { + showPopover.value = false; + }, + child: EnvVarPopover(suggestion: currentSuggestion, scope: scope), + ), + anchor: const Aligned( + follower: Alignment.bottomCenter, + target: Alignment.topCenter, + backup: Aligned( + follower: Alignment.topCenter, + target: Alignment.bottomCenter, + ), + ), + child: kIsMobile + ? TapRegion( + onTapInside: (_) { + showPopover.value = true; + }, + onTapOutside: (_) { + showPopover.value = false; + }, + child: text, + ) + : MouseRegion( + onEnter: (_) { + showPopover.value = true; + }, + onExit: (_) { + showPopover.value = false; + }, + child: text, + ), + ); + } +} diff --git a/lib/screens/common_widgets/sidebar_filter.dart b/lib/screens/common_widgets/sidebar_filter.dart new file mode 100644 index 00000000..478159b4 --- /dev/null +++ b/lib/screens/common_widgets/sidebar_filter.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; + +class SidebarFilter extends StatelessWidget { + const SidebarFilter({ + super.key, + this.onFilterFieldChanged, + this.filterHintText, + }); + + final Function(String)? onFilterFieldChanged; + final String? filterHintText; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: kBorderRadius8, + border: Border.all( + color: Theme.of(context).colorScheme.surfaceVariant, + ), + ), + child: Row( + children: [ + kHSpacer5, + Icon( + Icons.filter_alt, + size: 18, + color: Theme.of(context).colorScheme.secondary, + ), + kHSpacer5, + Expanded( + child: RawTextField( + style: Theme.of(context).textTheme.bodyMedium, + hintText: filterHintText ?? "Filter by name", + onChanged: onFilterFieldChanged, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/common_widgets/sidebar_header.dart b/lib/screens/common_widgets/sidebar_header.dart new file mode 100644 index 00000000..e0cd1d71 --- /dev/null +++ b/lib/screens/common_widgets/sidebar_header.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/consts.dart'; +import 'sidebar_save_button.dart'; + +class SidebarHeader extends ConsumerWidget { + const SidebarHeader({super.key, this.onAddNew}); + final Function()? onAddNew; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mobileScaffoldKey = ref.read(mobileScaffoldKeyStateProvider); + return Padding( + padding: kPe8, + child: Row( + children: [ + const SaveButton(), + const Spacer(), + ElevatedButton( + onPressed: onAddNew, + child: const Text( + kLabelPlusNew, + style: kTextStyleButton, + ), + ), + context.width <= kMinWindowSize.width + ? IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.all(4), + minimumSize: const Size(30, 30)), + onPressed: () { + mobileScaffoldKey.currentState?.closeDrawer(); + }, + icon: const Icon(Icons.chevron_left), + ) + : const SizedBox.shrink(), + ], + ), + ); + } +} diff --git a/lib/screens/common_widgets/sidebar_save_button.dart b/lib/screens/common_widgets/sidebar_save_button.dart new file mode 100644 index 00000000..1a04b197 --- /dev/null +++ b/lib/screens/common_widgets/sidebar_save_button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; + +class SaveButton extends ConsumerWidget { + const SaveButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final overlayWidget = OverlayWidgetTemplate(context: context); + final savingData = ref.watch(saveDataStateProvider); + final hasUnsavedChanges = ref.watch(hasUnsavedChangesProvider); + return TextButton.icon( + onPressed: (savingData || !hasUnsavedChanges) + ? null + : () async { + overlayWidget.show( + widget: const SavingOverlay(saveCompleted: false)); + + await ref + .read(collectionStateNotifierProvider.notifier) + .saveData(); + await ref + .read(environmentsStateNotifierProvider.notifier) + .saveEnvironments(); + overlayWidget.hide(); + overlayWidget.show( + widget: const SavingOverlay(saveCompleted: true)); + await Future.delayed(const Duration(seconds: 1)); + overlayWidget.hide(); + }, + icon: const Icon( + Icons.save, + size: 20, + ), + label: const Text( + kLabelSave, + style: kTextStyleButton, + ), + ); + } +} diff --git a/lib/screens/envvar/environment_editor.dart b/lib/screens/envvar/environment_editor.dart index 5136b3ae..53b17206 100644 --- a/lib/screens/envvar/environment_editor.dart +++ b/lib/screens/envvar/environment_editor.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -import '../common/main_editor_widgets.dart'; +import '../common_widgets/common_widgets.dart'; import './editor_pane/variables_pane.dart'; class EnvironmentEditor extends ConsumerWidget { @@ -39,7 +40,7 @@ class EnvironmentEditor extends ConsumerWidget { const SizedBox( width: 6, ), - TitleActionsArray( + EditorTitleActions( onRenamePressed: () { showRenameDialog(context, "Rename Environment", name, (val) { diff --git a/lib/screens/envvar/environment_page.dart b/lib/screens/envvar/environment_page.dart index 35bbc11c..bbf1dcec 100644 --- a/lib/screens/envvar/environment_page.dart +++ b/lib/screens/envvar/environment_page.dart @@ -5,7 +5,7 @@ import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; -import '../common/main_editor_widgets.dart'; +import '../common_widgets/common_widgets.dart'; import 'environments_pane.dart'; import 'environment_editor.dart'; @@ -22,10 +22,10 @@ class EnvironmentPage extends ConsumerWidget { final name = getEnvironmentTitle(ref.watch( selectedEnvironmentModelProvider.select((value) => value?.name))); if (context.isMediumWindow) { - return TwoDrawerScaffold( + return DrawerSplitView( scaffoldKey: scaffoldKey, mainContent: const EnvironmentEditor(), - title: ScaffoldTitle( + title: EditorTitle( title: name, showMenu: id != kGlobalEnvironmentId, onSelected: (ItemMenuOption item) { diff --git a/lib/screens/envvar/environments_pane.dart b/lib/screens/envvar/environments_pane.dart index 84a40528..340cc0e8 100644 --- a/lib/screens/envvar/environments_pane.dart +++ b/lib/screens/envvar/environments_pane.dart @@ -6,7 +6,7 @@ import 'package:apidash/models/environment_model.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -import '../common/sidebar_widgets.dart'; +import '../common_widgets/common_widgets.dart'; class EnvironmentsPane extends ConsumerWidget { const EnvironmentsPane({ diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 685ecdeb..ef46dc95 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -5,7 +5,7 @@ import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; -import 'package:apidash/screens/common/sidebar_widgets.dart'; +import '../common_widgets/common_widgets.dart'; class CollectionPane extends ConsumerWidget { const CollectionPane({ diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart index c85d699b..cce80d1c 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart @@ -6,7 +6,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; -import 'package:apidash/screens/common/environment_textfields.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; class EditRequestHeaders extends ConsumerStatefulWidget { const EditRequestHeaders({super.key}); diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart index 76aeb180..e3f42a5c 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart @@ -6,7 +6,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; -import 'package:apidash/screens/common/environment_textfields.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; class EditRequestURLParams extends ConsumerStatefulWidget { const EditRequestURLParams({super.key}); diff --git a/lib/screens/home_page/editor_pane/editor_request.dart b/lib/screens/home_page/editor_pane/editor_request.dart index d62d53a5..9d01fe2f 100644 --- a/lib/screens/home_page/editor_pane/editor_request.dart +++ b/lib/screens/home_page/editor_pane/editor_request.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import 'details_card/details_card.dart'; import 'details_card/request_pane/request_pane.dart'; -import '../../common/main_editor_widgets.dart'; +import '../../common_widgets/common_widgets.dart'; import 'url_card.dart'; class RequestEditor extends StatelessWidget { @@ -69,7 +70,7 @@ class RequestEditorTopBar extends ConsumerWidget { const SizedBox( width: 6, ), - TitleActionsArray( + EditorTitleActions( onRenamePressed: () { showRenameDialog(context, "Rename Request", name, (val) { ref diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index fd72d373..0cfc070e 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -4,7 +4,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/extensions/extensions.dart'; -import '../../common/environment_textfields.dart'; +import '../../common_widgets/common_widgets.dart'; class EditorPaneRequestURLCard extends StatelessWidget { const EditorPaneRequestURLCard({super.key}); @@ -45,7 +45,7 @@ class EditorPaneRequestURLCard extends StatelessWidget { kHSpacer20, SizedBox( height: 36, - child: SendButton(), + child: SendRequestButton(), ) ], ), @@ -104,9 +104,9 @@ class URLTextField extends ConsumerWidget { } } -class SendButton extends ConsumerWidget { +class SendRequestButton extends ConsumerWidget { final Function()? onTap; - const SendButton({ + const SendRequestButton({ super.key, this.onTap, }); @@ -117,7 +117,7 @@ class SendButton extends ConsumerWidget { final isWorking = ref.watch( selectedRequestModelProvider.select((value) => value?.isWorking)); - return SendRequestButton( + return SendButton( isWorking: isWorking ?? false, onTap: () { onTap?.call(); diff --git a/lib/screens/mobile/requests_page/request_response_tabs.dart b/lib/screens/mobile/requests_page/request_response_tabs.dart index 33b42742..38fab16a 100644 --- a/lib/screens/mobile/requests_page/request_response_tabs.dart +++ b/lib/screens/mobile/requests_page/request_response_tabs.dart @@ -42,16 +42,16 @@ class RequestResponseTabbar extends StatelessWidget { Widget build(BuildContext context) { return Center( child: Container( - width: 280, - height: 32, + width: kReqResTabWidth, + height: kReqResTabHeight, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50), + borderRadius: kBorderRadius20, border: Border.all( color: Theme.of(context).colorScheme.outlineVariant, ), ), child: ClipRRect( - borderRadius: BorderRadius.circular(50), + borderRadius: kBorderRadius20, child: TabBar( dividerColor: Colors.transparent, indicatorWeight: 0.0, @@ -63,18 +63,18 @@ class RequestResponseTabbar extends StatelessWidget { color: Theme.of(context).colorScheme.onPrimary, ), unselectedLabelStyle: kTextStyleTab, - splashBorderRadius: BorderRadius.circular(50), + splashBorderRadius: kBorderRadius20, indicator: BoxDecoration( - borderRadius: BorderRadius.circular(50), + borderRadius: kBorderRadius20, color: Theme.of(context).colorScheme.primary, ), controller: controller, tabs: const [ Tab( - text: "Request", + text: kLabelRequest, ), Tab( - text: "Response", + text: kLabelResponse, ), ], ), diff --git a/lib/screens/mobile/requests_page/requests_page.dart b/lib/screens/mobile/requests_page/requests_page.dart index d506b6d4..86d4d70d 100644 --- a/lib/screens/mobile/requests_page/requests_page.dart +++ b/lib/screens/mobile/requests_page/requests_page.dart @@ -9,7 +9,7 @@ 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/main_editor_widgets.dart'; +import '../../common_widgets/common_widgets.dart'; import '../widgets/page_base.dart'; import 'request_response_tabs.dart'; @@ -35,9 +35,9 @@ class _RequestResponsePageState extends ConsumerState ref.watch(selectedRequestModelProvider.select((value) => value?.name))); final TabController requestResponseTabController = useTabController(initialLength: 2, vsync: this); - return TwoDrawerScaffold( + return DrawerSplitView( scaffoldKey: widget.scaffoldKey, - title: ScaffoldTitle( + title: EditorTitle( title: name, onSelected: (ItemMenuOption item) { if (item == ItemMenuOption.edit) { @@ -125,12 +125,15 @@ class RequestResponsePageBottombar extends ConsumerWidget { }, icon: const Icon(Icons.code_rounded), ), - SendButton( - onTap: () { - if (requestResponseTabController.index != 1) { - requestResponseTabController.animateTo(1); - } - }, + SizedBox( + height: 36, + child: SendRequestButton( + onTap: () { + if (requestResponseTabController.index != 1) { + requestResponseTabController.animateTo(1); + } + }, + ), ), ], ), diff --git a/lib/utils/envvar_utils.dart b/lib/utils/envvar_utils.dart index 27eb32ae..f3157dce 100644 --- a/lib/utils/envvar_utils.dart +++ b/lib/utils/envvar_utils.dart @@ -1,8 +1,8 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/models/models.dart'; -import 'package:apidash/screens/common/environment_autocomplete.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; +import '../screens/common_widgets/common_widgets.dart'; String getEnvironmentTitle(String? name) { if (name == null || name.trim() == "") { @@ -126,7 +126,7 @@ List<(String, Object?, Widget?)> getMentions( mentions.add(( '{{${variable?.key ?? variableName}}}', suggestion, - EnvironmentVariableSpan(suggestion: suggestion) + EnvVarSpan(suggestion: suggestion) )); } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index f0d5f8f8..1857825d 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,5 +1,6 @@ export 'ui_utils.dart'; export 'convert_utils.dart'; +export 'header_utils.dart'; export 'http_utils.dart'; export 'file_utils.dart'; export 'window_utils.dart'; diff --git a/lib/widgets/button_clear_response.dart b/lib/widgets/button_clear_response.dart new file mode 100644 index 00000000..348c0913 --- /dev/null +++ b/lib/widgets/button_clear_response.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class ClearResponseButton extends StatelessWidget { + const ClearResponseButton({ + super.key, + this.onPressed, + }); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return IconButton( + tooltip: kTooltipClearResponse, + onPressed: onPressed, + icon: const Icon( + Icons.delete, + size: 16, + ), + ); + } +} diff --git a/lib/widgets/button_copy.dart b/lib/widgets/button_copy.dart new file mode 100644 index 00000000..8c3d56de --- /dev/null +++ b/lib/widgets/button_copy.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:apidash/consts.dart'; +import "snackbars.dart"; + +class CopyButton extends StatelessWidget { + const CopyButton({ + super.key, + required this.toCopy, + this.showLabel = true, + }); + + final String toCopy; + final bool showLabel; + + @override + Widget build(BuildContext context) { + var sm = ScaffoldMessenger.of(context); + return Tooltip( + message: showLabel ? '' : kLabelCopy, + child: SizedBox( + width: showLabel ? null : kTextButtonMinWidth, + child: TextButton( + onPressed: () async { + await Clipboard.setData(ClipboardData(text: toCopy)); + sm.hideCurrentSnackBar(); + sm.showSnackBar(getSnackBar("Copied")); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.content_copy, + size: 20, + ), + if (showLabel) const Text(kLabelCopy) + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/button_discord.dart b/lib/widgets/button_discord.dart new file mode 100644 index 00000000..1b348944 --- /dev/null +++ b/lib/widgets/button_discord.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:apidash/consts.dart'; + +class DiscordButton extends StatelessWidget { + const DiscordButton({ + super.key, + this.text, + }); + + final String? text; + + @override + Widget build(BuildContext context) { + var label = text ?? 'Discord Server'; + return FilledButton.icon( + onPressed: () { + launchUrl(Uri.parse(kDiscordUrl)); + }, + icon: const Icon( + Icons.discord, + size: 20.0, + ), + label: Text( + label, + style: kTextStyleButton, + ), + ); + } +} diff --git a/lib/widgets/button_repo.dart b/lib/widgets/button_repo.dart new file mode 100644 index 00000000..2ff81696 --- /dev/null +++ b/lib/widgets/button_repo.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:apidash/consts.dart'; + +class RepoButton extends StatelessWidget { + const RepoButton({ + super.key, + this.text, + this.icon, + }); + + final String? text; + final IconData? icon; + + @override + Widget build(BuildContext context) { + var label = text ?? "GitHub"; + if (icon == null) { + return FilledButton( + onPressed: () { + launchUrl(Uri.parse(kGitUrl)); + }, + child: Text( + label, + style: kTextStyleButton, + ), + ); + } + return FilledButton.icon( + onPressed: () { + launchUrl(Uri.parse(kGitUrl)); + }, + icon: Icon( + icon, + size: 20.0, + ), + label: Text( + label, + style: kTextStyleButton, + ), + ); + } +} diff --git a/lib/widgets/button_save_download.dart b/lib/widgets/button_save_download.dart new file mode 100644 index 00000000..b3ffbb31 --- /dev/null +++ b/lib/widgets/button_save_download.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; +import "snackbars.dart"; + +class SaveInDownloadsButton extends StatelessWidget { + const SaveInDownloadsButton({ + super.key, + this.content, + this.mimeType, + this.ext, + this.name, + this.showLabel = true, + }); + + final Uint8List? content; + final String? mimeType; + final String? ext; + final String? name; + final bool showLabel; + + @override + Widget build(BuildContext context) { + var sm = ScaffoldMessenger.of(context); + return Tooltip( + message: showLabel ? '' : kLabelDownload, + child: SizedBox( + width: showLabel ? null : kTextButtonMinWidth, + child: TextButton( + onPressed: (content != null) + ? () async { + var message = ""; + var path = await getFileDownloadpath( + name, + ext ?? getFileExtension(mimeType), + ); + if (path != null) { + try { + await saveFile(path, content!); + var sp = getShortPath(path); + message = 'Saved to $sp'; + } catch (e) { + message = "An error occurred while saving file."; + } + } else { + message = "Unable to determine the download path."; + } + sm.hideCurrentSnackBar(); + sm.showSnackBar(getSnackBar(message, small: false)); + } + : null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.download, + size: 20, + ), + if (showLabel) const Text(kLabelDownload) + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/button_send.dart b/lib/widgets/button_send.dart new file mode 100644 index 00000000..37d51dd3 --- /dev/null +++ b/lib/widgets/button_send.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class SendButton extends StatelessWidget { + const SendButton({ + super.key, + required this.isWorking, + required this.onTap, + }); + + final bool isWorking; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return FilledButton( + onPressed: isWorking ? null : onTap, + child: Row( + mainAxisSize: MainAxisSize.min, + children: isWorking + ? const [ + Text( + kLabelSending, + style: kTextStyleButton, + ), + ] + : const [ + Text( + kLabelSend, + style: kTextStyleButton, + ), + kHSpacer10, + Icon( + size: 16, + Icons.send, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart deleted file mode 100644 index e3b1d2d3..00000000 --- a/lib/widgets/buttons.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:apidash/utils/utils.dart'; -import 'package:apidash/consts.dart'; -import "snackbars.dart"; - -class CopyButton extends StatelessWidget { - const CopyButton({ - super.key, - required this.toCopy, - this.showLabel = true, - }); - - final String toCopy; - final bool showLabel; - - @override - Widget build(BuildContext context) { - var sm = ScaffoldMessenger.of(context); - return Tooltip( - message: showLabel ? '' : kLabelCopy, - child: SizedBox( - width: showLabel ? null : kTextButtonMinWidth, - child: TextButton( - onPressed: () async { - await Clipboard.setData(ClipboardData(text: toCopy)); - sm.hideCurrentSnackBar(); - sm.showSnackBar(getSnackBar("Copied")); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.content_copy, - size: 20, - ), - if (showLabel) const Text(kLabelCopy) - ], - ), - ), - ), - ); - } -} - -class SendRequestButton extends StatelessWidget { - const SendRequestButton({ - super.key, - required this.isWorking, - required this.onTap, - }); - - final bool isWorking; - final void Function() onTap; - - @override - Widget build(BuildContext context) { - return FilledButton( - onPressed: isWorking ? null : onTap, - child: Row( - mainAxisSize: MainAxisSize.min, - children: isWorking - ? const [ - Text( - kLabelSending, - style: kTextStyleButton, - ), - ] - : const [ - Text( - kLabelSend, - style: kTextStyleButton, - ), - kHSpacer10, - Icon( - size: 16, - Icons.send, - ), - ], - ), - ); - } -} - -class SaveInDownloadsButton extends StatelessWidget { - const SaveInDownloadsButton({ - super.key, - this.content, - this.mimeType, - this.ext, - this.name, - this.showLabel = true, - }); - - final Uint8List? content; - final String? mimeType; - final String? ext; - final String? name; - final bool showLabel; - - @override - Widget build(BuildContext context) { - var sm = ScaffoldMessenger.of(context); - return Tooltip( - message: showLabel ? '' : kLabelDownload, - child: SizedBox( - width: showLabel ? null : kTextButtonMinWidth, - child: TextButton( - onPressed: (content != null) - ? () async { - var message = ""; - var path = await getFileDownloadpath( - name, - ext ?? getFileExtension(mimeType), - ); - if (path != null) { - try { - await saveFile(path, content!); - var sp = getShortPath(path); - message = 'Saved to $sp'; - } catch (e) { - message = "An error occurred while saving file."; - } - } else { - message = "Unable to determine the download path."; - } - sm.hideCurrentSnackBar(); - sm.showSnackBar(getSnackBar(message, small: false)); - } - : null, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.download, - size: 20, - ), - if (showLabel) const Text(kLabelDownload) - ], - ), - ), - ), - ); - } -} - -class RepoButton extends StatelessWidget { - const RepoButton({ - super.key, - this.text, - this.icon, - }); - - final String? text; - final IconData? icon; - - @override - Widget build(BuildContext context) { - var label = text ?? "GitHub"; - if (icon == null) { - return FilledButton( - onPressed: () { - launchUrl(Uri.parse(kGitUrl)); - }, - child: Text( - label, - style: kTextStyleButton, - ), - ); - } - return FilledButton.icon( - onPressed: () { - launchUrl(Uri.parse(kGitUrl)); - }, - icon: Icon( - icon, - size: 20.0, - ), - label: Text( - label, - style: kTextStyleButton, - ), - ); - } -} - -class DiscordButton extends StatelessWidget { - const DiscordButton({ - super.key, - this.text, - }); - - final String? text; - - @override - Widget build(BuildContext context) { - var label = text ?? 'Discord Server'; - return FilledButton.icon( - onPressed: () { - launchUrl(Uri.parse(kDiscordUrl)); - }, - icon: const Icon( - Icons.discord, - size: 20.0, - ), - label: Text( - label, - style: kTextStyleButton, - ), - ); - } -} - -class ClearResponseButton extends StatelessWidget { - const ClearResponseButton({ - super.key, - this.onPressed, - }); - - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) { - return IconButton( - tooltip: kTooltipClearResponse, - onPressed: onPressed, - icon: const Icon( - Icons.delete, - size: 16, - ), - ); - } -} diff --git a/lib/widgets/card_request_details.dart b/lib/widgets/card_request_details.dart new file mode 100644 index 00000000..b12bc82e --- /dev/null +++ b/lib/widgets/card_request_details.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class RequestDetailsCard extends StatelessWidget { + const RequestDetailsCard({super.key, this.child}); + + final Widget? child; + @override + @override + Widget build(BuildContext context) { + return Card( + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.surfaceVariant, + ), + borderRadius: kBorderRadius12, + ), + elevation: 0, + child: child, + ); + } +} diff --git a/lib/widgets/card_sidebar_environment.dart b/lib/widgets/card_sidebar_environment.dart new file mode 100644 index 00000000..dadd3090 --- /dev/null +++ b/lib/widgets/card_sidebar_environment.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/utils/utils.dart'; +import 'menus.dart' show ItemCardMenu; + +class SidebarEnvironmentCard extends StatelessWidget { + const SidebarEnvironmentCard({ + super.key, + required this.id, + this.isGlobal = false, + this.isActive = false, + this.name, + this.selectedId, + this.editRequestId, + this.setActive, + this.onTap, + this.onDoubleTap, + this.onSecondaryTap, + this.onChangedNameEditor, + this.focusNode, + this.onTapOutsideNameEditor, + this.onMenuSelected, + }); + + final String id; + final bool isGlobal; + final bool isActive; + final String? name; + final String? selectedId; + final String? editRequestId; + final void Function(bool?)? setActive; + final void Function()? onTap; + final void Function()? onDoubleTap; + final void Function()? onSecondaryTap; + final Function(String)? onChangedNameEditor; + final FocusNode? focusNode; + final Function()? onTapOutsideNameEditor; + final Function(ItemMenuOption)? onMenuSelected; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final Color color = + isGlobal ? colorScheme.secondaryContainer : colorScheme.surface; + final Color colorVariant = colorScheme.surfaceVariant.withOpacity(0.5); + final Color surfaceTint = colorScheme.primary; + bool isSelected = selectedId == id; + bool inEditMode = editRequestId == id; + String nm = getEnvironmentTitle(name); + return Tooltip( + message: nm, + 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 && !isGlobal + ? colorScheme.brightness == Brightness.dark + ? colorVariant + : color + : color, + margin: EdgeInsets.zero, + child: InkWell( + borderRadius: kBorderRadius8, + hoverColor: colorVariant, + focusColor: colorVariant.withOpacity(0.5), + onTap: inEditMode ? null : onTap, + onSecondaryTap: onSecondaryTap, + child: Padding( + padding: EdgeInsets.only( + left: 6, + right: isSelected ? 6 : 10, + top: 5, + bottom: 5, + ), + child: SizedBox( + height: 20, + child: Row( + children: [ + kHSpacer4, + Expanded( + child: inEditMode + ? TextFormField( + key: ValueKey("$id-name"), + initialValue: name, + focusNode: focusNode, + style: Theme.of(context).textTheme.bodyMedium, + onTapOutside: (_) { + onTapOutsideNameEditor?.call(); + }, + onFieldSubmitted: (value) { + onTapOutsideNameEditor?.call(); + }, + onChanged: onChangedNameEditor, + decoration: const InputDecoration( + isCollapsed: true, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ) + : Text( + nm, + softWrap: false, + overflow: TextOverflow.fade, + ), + ), + Visibility( + visible: isSelected && !inEditMode && !isGlobal, + child: SizedBox( + width: 28, + child: ItemCardMenu( + onSelected: onMenuSelected, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/cards.dart b/lib/widgets/card_sidebar_request.dart similarity index 50% rename from lib/widgets/cards.dart rename to lib/widgets/card_sidebar_request.dart index 03861925..63cd1f7b 100644 --- a/lib/widgets/cards.dart +++ b/lib/widgets/card_sidebar_request.dart @@ -132,146 +132,3 @@ class SidebarRequestCard extends StatelessWidget { ); } } - -class RequestDetailsCard extends StatelessWidget { - const RequestDetailsCard({super.key, this.child}); - - final Widget? child; - @override - @override - Widget build(BuildContext context) { - return Card( - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).colorScheme.surfaceVariant, - ), - borderRadius: kBorderRadius12, - ), - elevation: 0, - child: child, - ); - } -} - -class SidebarEnvironmentCard extends StatelessWidget { - const SidebarEnvironmentCard({ - super.key, - required this.id, - this.isGlobal = false, - this.isActive = false, - this.name, - this.selectedId, - this.editRequestId, - this.setActive, - this.onTap, - this.onDoubleTap, - this.onSecondaryTap, - this.onChangedNameEditor, - this.focusNode, - this.onTapOutsideNameEditor, - this.onMenuSelected, - }); - - final String id; - final bool isGlobal; - final bool isActive; - final String? name; - final String? selectedId; - final String? editRequestId; - final void Function(bool?)? setActive; - final void Function()? onTap; - final void Function()? onDoubleTap; - final void Function()? onSecondaryTap; - final Function(String)? onChangedNameEditor; - final FocusNode? focusNode; - final Function()? onTapOutsideNameEditor; - final Function(ItemMenuOption)? onMenuSelected; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final Color color = - isGlobal ? colorScheme.secondaryContainer : colorScheme.surface; - final Color colorVariant = colorScheme.surfaceVariant.withOpacity(0.5); - final Color surfaceTint = colorScheme.primary; - bool isSelected = selectedId == id; - bool inEditMode = editRequestId == id; - String nm = getEnvironmentTitle(name); - return Tooltip( - message: nm, - 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 && !isGlobal - ? colorScheme.brightness == Brightness.dark - ? colorVariant - : color - : color, - margin: EdgeInsets.zero, - child: InkWell( - borderRadius: kBorderRadius8, - hoverColor: colorVariant, - focusColor: colorVariant.withOpacity(0.5), - onTap: inEditMode ? null : onTap, - onSecondaryTap: onSecondaryTap, - child: Padding( - padding: EdgeInsets.only( - left: 6, - right: isSelected ? 6 : 10, - top: 5, - bottom: 5, - ), - child: SizedBox( - height: 20, - child: Row( - children: [ - kHSpacer4, - Expanded( - child: inEditMode - ? TextFormField( - key: ValueKey("$id-name"), - initialValue: name, - focusNode: focusNode, - style: Theme.of(context).textTheme.bodyMedium, - onTapOutside: (_) { - onTapOutsideNameEditor?.call(); - }, - onFieldSubmitted: (value) { - onTapOutsideNameEditor?.call(); - }, - onChanged: onChangedNameEditor, - decoration: const InputDecoration( - isCollapsed: true, - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - ), - ) - : Text( - nm, - softWrap: false, - overflow: TextOverflow.fade, - ), - ), - Visibility( - visible: isSelected && !inEditMode && !isGlobal, - child: SizedBox( - width: 28, - child: ItemCardMenu( - onSelected: onMenuSelected, - ), - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/widgets/dialogs.dart b/lib/widgets/dialogs.dart new file mode 100644 index 00000000..900ba758 --- /dev/null +++ b/lib/widgets/dialogs.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +showRenameDialog( + BuildContext context, + String dialogTitle, + String? name, + Function(String) onRename, +) { + showDialog( + context: context, + builder: (context) { + final controller = TextEditingController(text: name ?? ""); + controller.selection = + TextSelection(baseOffset: 0, extentOffset: controller.text.length); + return AlertDialog( + title: Text(dialogTitle), + content: TextField( + autofocus: true, + controller: controller, + decoration: const InputDecoration(hintText: "Enter new name"), + ), + actions: [ + OutlinedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('CANCEL')), + FilledButton( + onPressed: () { + final val = controller.text.trim(); + onRename(val); + Navigator.pop(context); + Future.delayed(const Duration(milliseconds: 100), () { + controller.dispose(); + }); + }, + child: const Text('OK')), + ], + ); + }); +} diff --git a/lib/widgets/dropdown_codegen.dart b/lib/widgets/dropdown_codegen.dart new file mode 100644 index 00000000..4e158377 --- /dev/null +++ b/lib/widgets/dropdown_codegen.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class DropdownButtonCodegenLanguage extends StatelessWidget { + const DropdownButtonCodegenLanguage({ + super.key, + this.codegenLanguage, + this.onChanged, + }); + + final CodegenLanguage? codegenLanguage; + final void Function(CodegenLanguage?)? onChanged; + + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + isExpanded: true, + focusColor: surfaceColor, + value: codegenLanguage, + icon: const Icon( + Icons.unfold_more_rounded, + size: 16, + ), + elevation: 4, + style: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + underline: Container( + height: 0, + ), + onChanged: onChanged, + borderRadius: kBorderRadius12, + items: CodegenLanguage.values + .map>((CodegenLanguage value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kPs8, + child: Text( + value.label, + style: kTextStyleButton, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/widgets/dropdown_content_type.dart b/lib/widgets/dropdown_content_type.dart new file mode 100644 index 00000000..952dcad1 --- /dev/null +++ b/lib/widgets/dropdown_content_type.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class DropdownButtonContentType extends StatelessWidget { + const DropdownButtonContentType({ + super.key, + this.contentType, + this.onChanged, + }); + + final ContentType? contentType; + final void Function(ContentType?)? onChanged; + + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + focusColor: surfaceColor, + value: contentType, + icon: const Icon( + Icons.unfold_more_rounded, + size: 16, + ), + elevation: 4, + style: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + underline: Container( + height: 0, + ), + onChanged: onChanged, + borderRadius: kBorderRadius12, + items: ContentType.values + .map>((ContentType value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kPs8, + child: Text( + value.name, + style: kTextStyleButton, + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/widgets/dropdown_formdata.dart b/lib/widgets/dropdown_formdata.dart new file mode 100644 index 00000000..02645afa --- /dev/null +++ b/lib/widgets/dropdown_formdata.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class DropdownButtonFormData extends StatelessWidget { + const DropdownButtonFormData({ + super.key, + this.formDataType, + this.onChanged, + }); + + final FormDataType? formDataType; + final void Function(FormDataType?)? onChanged; + + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + dropdownColor: surfaceColor, + focusColor: surfaceColor, + value: formDataType, + icon: const Icon( + Icons.unfold_more_rounded, + size: 16, + ), + elevation: 4, + style: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + underline: const IgnorePointer(), + onChanged: onChanged, + borderRadius: kBorderRadius12, + items: FormDataType.values + .map>((FormDataType value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kPs8, + child: Text( + value.name, + style: kTextStyleButton, + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/widgets/dropdown_http_method.dart b/lib/widgets/dropdown_http_method.dart new file mode 100644 index 00000000..81623325 --- /dev/null +++ b/lib/widgets/dropdown_http_method.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/extensions/extensions.dart'; + +class DropdownButtonHttpMethod extends StatelessWidget { + const DropdownButtonHttpMethod({ + super.key, + this.method, + this.onChanged, + }); + + final HTTPVerb? method; + final void Function(HTTPVerb? value)? onChanged; + + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + focusColor: surfaceColor, + value: method, + icon: const Icon(Icons.unfold_more_rounded), + elevation: 4, + underline: Container( + height: 0, + ), + borderRadius: kBorderRadius12, + onChanged: onChanged, + items: HTTPVerb.values.map>((HTTPVerb value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: EdgeInsets.only(left: context.isMediumWindow ? 8 : 16), + child: Text( + value.name.toUpperCase(), + style: kCodeStyle.copyWith( + fontWeight: FontWeight.bold, + color: getHTTPMethodColor( + value, + brightness: Theme.of(context).brightness, + ), + ), + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/widgets/dropdowns.dart b/lib/widgets/dropdowns.dart deleted file mode 100644 index d21c3be4..00000000 --- a/lib/widgets/dropdowns.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:apidash/utils/utils.dart'; -import 'package:apidash/consts.dart'; -import 'package:apidash/extensions/extensions.dart'; - -class DropdownButtonHttpMethod extends StatelessWidget { - const DropdownButtonHttpMethod({ - super.key, - this.method, - this.onChanged, - }); - - final HTTPVerb? method; - final void Function(HTTPVerb? value)? onChanged; - - @override - Widget build(BuildContext context) { - final surfaceColor = Theme.of(context).colorScheme.surface; - return DropdownButton( - focusColor: surfaceColor, - value: method, - icon: const Icon(Icons.unfold_more_rounded), - elevation: 4, - underline: Container( - height: 0, - ), - borderRadius: kBorderRadius12, - onChanged: onChanged, - items: HTTPVerb.values.map>((HTTPVerb value) { - return DropdownMenuItem( - value: value, - child: Padding( - padding: EdgeInsets.only(left: context.isMediumWindow ? 8 : 16), - child: Text( - value.name.toUpperCase(), - style: kCodeStyle.copyWith( - fontWeight: FontWeight.bold, - color: getHTTPMethodColor( - value, - brightness: Theme.of(context).brightness, - ), - ), - ), - ), - ); - }).toList(), - ); - } -} - -class DropdownButtonContentType extends StatelessWidget { - const DropdownButtonContentType({ - super.key, - this.contentType, - this.onChanged, - }); - - final ContentType? contentType; - final void Function(ContentType?)? onChanged; - - @override - Widget build(BuildContext context) { - final surfaceColor = Theme.of(context).colorScheme.surface; - return DropdownButton( - focusColor: surfaceColor, - value: contentType, - icon: const Icon( - Icons.unfold_more_rounded, - size: 16, - ), - elevation: 4, - style: kCodeStyle.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - underline: Container( - height: 0, - ), - onChanged: onChanged, - borderRadius: kBorderRadius12, - items: ContentType.values - .map>((ContentType value) { - return DropdownMenuItem( - value: value, - child: Padding( - padding: kPs8, - child: Text( - value.name, - style: kTextStyleButton, - ), - ), - ); - }).toList(), - ); - } -} - -class DropdownButtonFormData extends StatelessWidget { - const DropdownButtonFormData({ - super.key, - this.formDataType, - this.onChanged, - }); - - final FormDataType? formDataType; - final void Function(FormDataType?)? onChanged; - - @override - Widget build(BuildContext context) { - final surfaceColor = Theme.of(context).colorScheme.surface; - return DropdownButton( - dropdownColor: surfaceColor, - focusColor: surfaceColor, - value: formDataType, - icon: const Icon( - Icons.unfold_more_rounded, - size: 16, - ), - elevation: 4, - style: kCodeStyle.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - underline: const IgnorePointer(), - onChanged: onChanged, - borderRadius: kBorderRadius12, - items: FormDataType.values - .map>((FormDataType value) { - return DropdownMenuItem( - value: value, - child: Padding( - padding: kPs8, - child: Text( - value.name, - style: kTextStyleButton, - ), - ), - ); - }).toList(), - ); - } -} - -class DropdownButtonCodegenLanguage extends StatelessWidget { - const DropdownButtonCodegenLanguage({ - super.key, - this.codegenLanguage, - this.onChanged, - }); - - final CodegenLanguage? codegenLanguage; - final void Function(CodegenLanguage?)? onChanged; - - @override - Widget build(BuildContext context) { - final surfaceColor = Theme.of(context).colorScheme.surface; - return DropdownButton( - isExpanded: true, - focusColor: surfaceColor, - value: codegenLanguage, - icon: const Icon( - Icons.unfold_more_rounded, - size: 16, - ), - elevation: 4, - style: kCodeStyle.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - underline: Container( - height: 0, - ), - onChanged: onChanged, - borderRadius: kBorderRadius12, - items: CodegenLanguage.values - .map>((CodegenLanguage value) { - return DropdownMenuItem( - value: value, - child: Padding( - padding: kPs8, - child: Text( - value.label, - style: kTextStyleButton, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ); - }).toList(), - ); - } -} diff --git a/lib/widgets/environment_field.dart b/lib/widgets/environment_field.dart deleted file mode 100644 index 3f95bb26..00000000 --- a/lib/widgets/environment_field.dart +++ /dev/null @@ -1,261 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_portal/flutter_portal.dart'; -import 'package:mention_tag_text_field/mention_tag_text_field.dart'; -import 'package:apidash/models/models.dart'; -import 'package:apidash/consts.dart'; -import '../screens/common/environment_autocomplete.dart'; - -class EnvironmentAutocompleteFieldBase extends StatefulHookWidget { - const EnvironmentAutocompleteFieldBase({ - super.key, - this.initialValue, - this.onChanged, - this.onFieldSubmitted, - this.style, - this.decoration, - this.initialMentions, - this.suggestions, - this.mentionValue, - required this.onMentionValueChanged, - }); - - final String? initialValue; - final void Function(String)? onChanged; - final void Function(String)? onFieldSubmitted; - final TextStyle? style; - final InputDecoration? decoration; - final List<(String, Object?, Widget?)>? initialMentions; - final List? suggestions; - final String? mentionValue; - final void Function(String?) onMentionValueChanged; - - @override - State createState() => - _EnvironmentAutocompleteFieldBaseState(); -} - -class _EnvironmentAutocompleteFieldBaseState - extends State { - final MentionTagTextEditingController controller = - MentionTagTextEditingController(); - - final FocusNode focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - controller.text = widget.initialValue ?? ""; - } - - @override - Widget build(BuildContext context) { - final isSuggestionsVisible = useState(false); - - return PortalTarget( - visible: isSuggestionsVisible.value && focusNode.hasFocus, - portalFollower: EnvironmentSuggestionsMenu( - mentionController: controller, - suggestions: widget.suggestions, - onSelect: (suggestion) { - controller.addMention( - label: '{${suggestion.variable.key}}}', - data: suggestion, - stylingWidget: EnvironmentVariableSpan(suggestion: suggestion)); - widget.onChanged?.call(controller.text); - widget.onMentionValueChanged.call(null); - isSuggestionsVisible.value = false; - var mentionsCharacters = - controller.mentions.fold(0, (previousValue, element) { - return previousValue + element.variable.key.length + 4 as int; - }); - controller.selection = TextSelection.collapsed( - offset: controller.text.length + - controller.mentions.length - - mentionsCharacters, - ); - }, - ), - anchor: const Aligned( - follower: Alignment.topLeft, - target: Alignment.bottomLeft, - backup: Aligned( - follower: Alignment.bottomLeft, - target: Alignment.topLeft, - ), - ), - child: EnvironmentMentionField( - focusNode: focusNode, - controller: controller, - initialMentions: widget.initialMentions ?? [], - mentionValue: widget.mentionValue, - onMentionValueChanged: widget.onMentionValueChanged, - isSuggestionsVisible: isSuggestionsVisible, - onChanged: widget.onChanged, - onFieldSubmitted: widget.onFieldSubmitted, - style: widget.style, - decoration: widget.decoration, - ), - ); - } -} - -class EnvironmentMentionField extends StatelessWidget { - const EnvironmentMentionField({ - super.key, - required this.focusNode, - required this.controller, - required this.initialMentions, - required this.mentionValue, - required this.onMentionValueChanged, - required this.isSuggestionsVisible, - this.onChanged, - this.onFieldSubmitted, - this.style, - this.decoration, - }); - - final FocusNode focusNode; - final MentionTagTextEditingController controller; - final List<(String, Object?, Widget?)> initialMentions; - final String? mentionValue; - final void Function(String?) onMentionValueChanged; - final ValueNotifier isSuggestionsVisible; - final void Function(String)? onChanged; - final void Function(String)? onFieldSubmitted; - final TextStyle? style; - final InputDecoration? decoration; - - void onMention(String? value) { - onMentionValueChanged.call(value); - if (value != null) { - isSuggestionsVisible.value = true; - } else { - isSuggestionsVisible.value = false; - } - } - - @override - Widget build(BuildContext context) { - return MentionTagTextFormField( - focusNode: focusNode, - onTap: () { - focusNode.requestFocus(); - }, - onTapOutside: (event) { - focusNode.unfocus(); - isSuggestionsVisible.value = false; - }, - controller: controller, - style: style, - initialMentions: initialMentions, - onMention: onMention, - onChanged: (value) { - onChanged?.call(controller.text); - }, - onFieldSubmitted: (value) { - onFieldSubmitted?.call(controller.text); - isSuggestionsVisible.value = false; - }, - decoration: decoration, - mentionTagDecoration: const MentionTagDecoration( - mentionStart: ['{'], - mentionBreak: - " ", // This is a workaround for the exception but adds a space after the mention - maxWords: 1, - allowDecrement: false, - allowEmbedding: true, - showMentionStartSymbol: false, - ), - ); - } -} - -class EnvironmentSuggestionsMenu extends StatelessWidget { - const EnvironmentSuggestionsMenu({ - super.key, - required this.mentionController, - required this.suggestions, - this.onSelect, - }); - - final MentionTagTextEditingController mentionController; - final List? suggestions; - final Function(EnvironmentVariableSuggestion)? onSelect; - - @override - Widget build(BuildContext context) { - return suggestions == null || suggestions!.isEmpty - ? const SizedBox.shrink() - : ClipRRect( - borderRadius: kBorderRadius8, - child: Material( - type: MaterialType.card, - elevation: 8, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: Ink( - width: 300, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: kBorderRadius8, - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - ), - ), - child: ListView.separated( - shrinkWrap: true, - itemCount: suggestions?.length ?? 0, - separatorBuilder: (context, index) => const Divider( - height: 2, - ), - itemBuilder: (context, index) { - final suggestion = suggestions![index]; - return ListTile( - dense: true, - leading: EnvironmentIndicator(suggestion: suggestion), - title: Text(suggestion.variable.key), - subtitle: Text(suggestion.variable.value), - onTap: () { - onSelect?.call(suggestions![index]); - }, - ); - }, - ), - ), - ), - ), - ); - } -} - -class EnvironmentIndicator extends StatelessWidget { - const EnvironmentIndicator({super.key, required this.suggestion}); - - final EnvironmentVariableSuggestion suggestion; - - @override - Widget build(BuildContext context) { - final isUnknown = suggestion.isUnknown; - final isGlobal = suggestion.environmentId == kGlobalEnvironmentId; - return Container( - padding: kP4, - decoration: BoxDecoration( - color: isUnknown - ? Theme.of(context).colorScheme.errorContainer - : isGlobal - ? Theme.of(context).colorScheme.secondaryContainer - : Theme.of(context).colorScheme.primaryContainer, - borderRadius: kBorderRadius4, - ), - child: Icon( - isUnknown - ? Icons.block - : isGlobal - ? Icons.public - : Icons.computer, - size: 16, - ), - ); - } -} diff --git a/lib/widgets/environment_field_base.dart b/lib/widgets/environment_field_base.dart new file mode 100644 index 00000000..fee6f587 --- /dev/null +++ b/lib/widgets/environment_field_base.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:mention_tag_text_field/mention_tag_text_field.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/widgets/field_mention.dart'; +import '../screens/common_widgets/common_widgets.dart'; +import 'suggestions_menu.dart'; + +class EnvironmentFieldBase extends StatefulHookWidget { + const EnvironmentFieldBase({ + super.key, + this.initialValue, + this.onChanged, + this.onFieldSubmitted, + this.style, + this.decoration, + this.initialMentions, + this.suggestions, + this.mentionValue, + required this.onMentionValueChanged, + }); + + final String? initialValue; + final void Function(String)? onChanged; + final void Function(String)? onFieldSubmitted; + final TextStyle? style; + final InputDecoration? decoration; + final List<(String, Object?, Widget?)>? initialMentions; + final List? suggestions; + final String? mentionValue; + final void Function(String?) onMentionValueChanged; + + @override + State createState() => + _EnvironmentAutocompleteFieldBaseState(); +} + +class _EnvironmentAutocompleteFieldBaseState + extends State { + final MentionTagTextEditingController controller = + MentionTagTextEditingController(); + + final FocusNode focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + controller.text = widget.initialValue ?? ""; + } + + @override + Widget build(BuildContext context) { + final isSuggestionsVisible = useState(false); + + return PortalTarget( + visible: isSuggestionsVisible.value && focusNode.hasFocus, + portalFollower: SuggestionsMenu( + mentionController: controller, + suggestions: widget.suggestions, + suggestionBuilder: (context, index) { + final suggestion = widget.suggestions![index]; + return ListTile( + dense: true, + leading: EnvVarIndicator(suggestion: suggestion), + title: Text(suggestion.variable.key), + subtitle: Text(suggestion.variable.value), + onTap: () { + controller.addMention( + label: '{${suggestion.variable.key}}}', + data: suggestion, + stylingWidget: EnvVarSpan(suggestion: suggestion)); + widget.onChanged?.call(controller.text); + widget.onMentionValueChanged.call(null); + isSuggestionsVisible.value = false; + }, + ); + }, + ), + anchor: const Aligned( + follower: Alignment.topLeft, + target: Alignment.bottomLeft, + backup: Aligned( + follower: Alignment.bottomLeft, + target: Alignment.topLeft, + ), + ), + child: MentionField( + focusNode: focusNode, + controller: controller, + initialMentions: widget.initialMentions ?? [], + mentionValue: widget.mentionValue, + onMentionValueChanged: widget.onMentionValueChanged, + isSuggestionsVisible: isSuggestionsVisible, + onChanged: widget.onChanged, + onFieldSubmitted: widget.onFieldSubmitted, + style: widget.style, + decoration: widget.decoration, + mentionStart: const ['{'], + maxWords: 1, + ), + ); + } +} diff --git a/lib/widgets/field_cell.dart b/lib/widgets/field_cell.dart new file mode 100644 index 00000000..86e10ad6 --- /dev/null +++ b/lib/widgets/field_cell.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class CellField extends StatelessWidget { + const CellField({ + super.key, + required this.keyId, + this.initialValue, + this.hintText, + this.onChanged, + this.colorScheme, + }); + + final String keyId; + final String? initialValue; + final String? hintText; + final void Function(String)? onChanged; + final ColorScheme? colorScheme; + + @override + Widget build(BuildContext context) { + var clrScheme = colorScheme ?? Theme.of(context).colorScheme; + return TextFormField( + key: Key(keyId), + initialValue: initialValue, + style: kCodeStyle.copyWith( + color: clrScheme.onSurface, + ), + decoration: InputDecoration( + hintStyle: kCodeStyle.copyWith( + color: clrScheme.outline.withOpacity( + kHintOpacity, + ), + ), + hintText: hintText, + contentPadding: const EdgeInsets.only(bottom: 12), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceVariant, + ), + ), + ), + onChanged: onChanged, + ); + } +} diff --git a/lib/widgets/field_cell_obscurable.dart b/lib/widgets/field_cell_obscurable.dart new file mode 100644 index 00000000..4526e7e2 --- /dev/null +++ b/lib/widgets/field_cell_obscurable.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class ObscurableCellField extends HookWidget { + const ObscurableCellField({ + super.key, + required this.keyId, + this.initialValue, + this.hintText, + this.onChanged, + this.colorScheme, + }); + + final String keyId; + final String? initialValue; + final String? hintText; + final void Function(String)? onChanged; + final ColorScheme? colorScheme; + + @override + Widget build(BuildContext context) { + final obscureText = useState(true); + var clrScheme = colorScheme ?? Theme.of(context).colorScheme; + return TextFormField( + key: Key(keyId), + initialValue: initialValue, + style: kCodeStyle.copyWith( + color: clrScheme.onSurface, + ), + obscureText: obscureText.value, + decoration: InputDecoration( + hintStyle: kCodeStyle.copyWith( + color: clrScheme.outline.withOpacity( + kHintOpacity, + ), + ), + hintText: hintText, + suffixIcon: IconButton( + padding: kP4, + icon: Icon( + obscureText.value ? Icons.visibility : Icons.visibility_off, + color: clrScheme.onSurface, + size: 14, + ), + onPressed: () { + obscureText.value = !obscureText.value; + }, + ), + contentPadding: const EdgeInsets.only(bottom: 12), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceVariant, + ), + ), + ), + onChanged: onChanged, + ); + } +} diff --git a/lib/widgets/headerfield.dart b/lib/widgets/field_header.dart similarity index 98% rename from lib/widgets/headerfield.dart rename to lib/widgets/field_header.dart index 3d60caac..9e237c2c 100644 --- a/lib/widgets/headerfield.dart +++ b/lib/widgets/field_header.dart @@ -1,7 +1,7 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/utils/header_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; class HeaderField extends StatefulWidget { const HeaderField({ diff --git a/lib/widgets/field_json_search.dart b/lib/widgets/field_json_search.dart new file mode 100644 index 00000000..b9a61992 --- /dev/null +++ b/lib/widgets/field_json_search.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'field_raw.dart'; + +class JsonSearchField extends StatelessWidget { + const JsonSearchField({super.key, this.onChanged, this.controller}); + + final void Function(String)? onChanged; + final TextEditingController? controller; + + @override + Widget build(BuildContext context) { + return RawTextField( + controller: controller, + onChanged: onChanged, + style: kCodeStyle, + hintText: 'Search..', + ); + } +} diff --git a/lib/widgets/field_mention.dart b/lib/widgets/field_mention.dart new file mode 100644 index 00000000..5a938178 --- /dev/null +++ b/lib/widgets/field_mention.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:mention_tag_text_field/mention_tag_text_field.dart'; + +class MentionField extends StatelessWidget { + const MentionField({ + super.key, + required this.focusNode, + required this.controller, + required this.initialMentions, + required this.mentionValue, + required this.onMentionValueChanged, + required this.isSuggestionsVisible, + this.onChanged, + this.onFieldSubmitted, + this.style, + this.decoration, + required this.mentionStart, + this.mentionBreak = "", + this.maxWords, + this.allowDecrement = false, + this.allowEmbedding = true, + this.showMentionStartSymbol = false, + }); + + final FocusNode focusNode; + final MentionTagTextEditingController controller; + final List<(String, Object?, Widget?)> initialMentions; + final String? mentionValue; + final void Function(String?) onMentionValueChanged; + final ValueNotifier isSuggestionsVisible; + final void Function(String)? onChanged; + final void Function(String)? onFieldSubmitted; + final TextStyle? style; + final InputDecoration? decoration; + final List mentionStart; + final String mentionBreak; + final int? maxWords; + final bool allowDecrement; + final bool allowEmbedding; + final bool showMentionStartSymbol; + + void onMention(String? value) { + onMentionValueChanged.call(value); + if (value != null) { + isSuggestionsVisible.value = true; + } else { + isSuggestionsVisible.value = false; + } + } + + @override + Widget build(BuildContext context) { + return MentionTagTextFormField( + focusNode: focusNode, + onTap: () { + focusNode.requestFocus(); + }, + onTapOutside: (event) { + focusNode.unfocus(); + isSuggestionsVisible.value = false; + }, + controller: controller, + style: style, + initialMentions: initialMentions, + onMention: onMention, + onChanged: (value) { + onChanged?.call(controller.text); + }, + onFieldSubmitted: (value) { + onFieldSubmitted?.call(controller.text); + isSuggestionsVisible.value = false; + }, + decoration: decoration, + mentionTagDecoration: MentionTagDecoration( + mentionStart: mentionStart, + mentionBreak: mentionBreak, + maxWords: maxWords, + allowDecrement: allowDecrement, + allowEmbedding: allowEmbedding, + showMentionStartSymbol: showMentionStartSymbol, + ), + ); + } +} diff --git a/lib/widgets/field_raw.dart b/lib/widgets/field_raw.dart new file mode 100644 index 00000000..cf7ef81e --- /dev/null +++ b/lib/widgets/field_raw.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class RawTextField extends StatelessWidget { + const RawTextField({ + super.key, + this.onChanged, + this.controller, + this.hintText, + this.style, + }); + + final void Function(String)? onChanged; + final TextEditingController? controller; + final String? hintText; + final TextStyle? style; + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + onChanged: onChanged, + style: style, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: hintText, + ), + onTapOutside: (PointerDownEvent event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + ); + } +} diff --git a/lib/widgets/field_url.dart b/lib/widgets/field_url.dart new file mode 100644 index 00000000..f20342bc --- /dev/null +++ b/lib/widgets/field_url.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class URLField extends StatelessWidget { + const URLField({ + super.key, + required this.selectedId, + this.initialValue, + this.onChanged, + this.onFieldSubmitted, + }); + + final String selectedId; + final String? initialValue; + final void Function(String)? onChanged; + final void Function(String)? onFieldSubmitted; + + @override + Widget build(BuildContext context) { + return TextFormField( + key: Key("url-$selectedId"), + initialValue: initialValue, + style: kCodeStyle, + decoration: InputDecoration( + hintText: kHintTextUrlCard, + hintStyle: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.outline.withOpacity( + kHintOpacity, + ), + ), + border: InputBorder.none, + ), + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + onTapOutside: (PointerDownEvent event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + ); + } +} diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 46d066bf..b52dc86d 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -7,7 +7,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import '../consts.dart'; import '../utils/ui_utils.dart'; import "snackbars.dart"; -import 'textfields.dart'; +import 'field_json_search.dart'; class JsonPreviewerColor { const JsonPreviewerColor._(); diff --git a/lib/widgets/markdown.dart b/lib/widgets/markdown.dart index ce06efa0..a3c4ec64 100644 --- a/lib/widgets/markdown.dart +++ b/lib/widgets/markdown.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:markdown/markdown.dart' as md; import 'package:url_launcher/url_launcher.dart'; -import 'buttons.dart'; +import 'button_discord.dart'; +import 'button_repo.dart'; class CustomMarkdown extends StatelessWidget { const CustomMarkdown({ diff --git a/lib/widgets/splitview_dashboard.dart b/lib/widgets/splitview_dashboard.dart new file mode 100644 index 00000000..4fd718e8 --- /dev/null +++ b/lib/widgets/splitview_dashboard.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:multi_split_view/multi_split_view.dart'; +import 'package:apidash/consts.dart'; + +class DashboardSplitView extends StatefulWidget { + const DashboardSplitView({ + super.key, + required this.sidebarWidget, + required this.mainWidget, + }); + + final Widget sidebarWidget; + final Widget mainWidget; + + @override + DashboardSplitViewState createState() => DashboardSplitViewState(); +} + +class DashboardSplitViewState extends State { + final MultiSplitViewController _controller = MultiSplitViewController( + areas: [ + Area(size: 250, minimalSize: 200), + Area(minimalWeight: 0.7), + ], + ); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MultiSplitViewTheme( + data: MultiSplitViewThemeData( + dividerThickness: 3, + dividerPainter: DividerPainters.background( + color: Theme.of(context).colorScheme.surfaceVariant, + highlightedColor: Theme.of(context).colorScheme.outline.withOpacity( + kHintOpacity, + ), + animationEnabled: false, + ), + ), + child: MultiSplitView( + controller: _controller, + children: [ + widget.sidebarWidget, + widget.mainWidget, + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/splitviews.dart b/lib/widgets/splitview_drawer.dart similarity index 50% rename from lib/widgets/splitviews.dart rename to lib/widgets/splitview_drawer.dart index 52f2aae7..80669d89 100644 --- a/lib/widgets/splitviews.dart +++ b/lib/widgets/splitview_drawer.dart @@ -1,119 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:multi_split_view/multi_split_view.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/consts.dart'; -class DashboardSplitView extends StatefulWidget { - const DashboardSplitView({ - super.key, - required this.sidebarWidget, - required this.mainWidget, - }); - - final Widget sidebarWidget; - final Widget mainWidget; - - @override - DashboardSplitViewState createState() => DashboardSplitViewState(); -} - -class DashboardSplitViewState extends State { - final MultiSplitViewController _controller = MultiSplitViewController( - areas: [ - Area(size: 250, minimalSize: 200), - Area(minimalWeight: 0.7), - ], - ); - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return MultiSplitViewTheme( - data: MultiSplitViewThemeData( - dividerThickness: 3, - dividerPainter: DividerPainters.background( - color: Theme.of(context).colorScheme.surfaceVariant, - highlightedColor: Theme.of(context).colorScheme.outline.withOpacity( - kHintOpacity, - ), - animationEnabled: false, - ), - ), - child: MultiSplitView( - controller: _controller, - children: [ - widget.sidebarWidget, - widget.mainWidget, - ], - ), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} - -class EqualSplitView extends StatefulWidget { - const EqualSplitView({ - super.key, - required this.leftWidget, - required this.rightWidget, - }); - - final Widget leftWidget; - final Widget rightWidget; - - @override - State createState() => _EqualSplitViewState(); -} - -class _EqualSplitViewState extends State { - final MultiSplitViewController _controller = MultiSplitViewController( - areas: [ - Area(minimalSize: kMinRequestEditorDetailsCardPaneSize), - Area(minimalSize: kMinRequestEditorDetailsCardPaneSize), - ], - ); - - @override - Widget build(BuildContext context) { - return MultiSplitViewTheme( - data: MultiSplitViewThemeData( - dividerThickness: 3, - dividerPainter: DividerPainters.background( - color: Theme.of(context).colorScheme.surfaceVariant, - highlightedColor: Theme.of(context).colorScheme.outline.withOpacity( - kHintOpacity, - ), - animationEnabled: false, - ), - ), - child: MultiSplitView( - controller: _controller, - children: [ - widget.leftWidget, - widget.rightWidget, - ], - ), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} - -class TwoDrawerScaffold extends StatelessWidget { - const TwoDrawerScaffold({ +class DrawerSplitView extends StatelessWidget { + const DrawerSplitView({ super.key, required this.scaffoldKey, required this.mainContent, diff --git a/lib/widgets/splitview_equal.dart b/lib/widgets/splitview_equal.dart new file mode 100644 index 00000000..affdf45b --- /dev/null +++ b/lib/widgets/splitview_equal.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:multi_split_view/multi_split_view.dart'; +import 'package:apidash/consts.dart'; + +class EqualSplitView extends StatefulWidget { + const EqualSplitView({ + super.key, + required this.leftWidget, + required this.rightWidget, + }); + + final Widget leftWidget; + final Widget rightWidget; + + @override + State createState() => _EqualSplitViewState(); +} + +class _EqualSplitViewState extends State { + final MultiSplitViewController _controller = MultiSplitViewController( + areas: [ + Area(minimalSize: kMinRequestEditorDetailsCardPaneSize), + Area(minimalSize: kMinRequestEditorDetailsCardPaneSize), + ], + ); + + @override + Widget build(BuildContext context) { + return MultiSplitViewTheme( + data: MultiSplitViewThemeData( + dividerThickness: 3, + dividerPainter: DividerPainters.background( + color: Theme.of(context).colorScheme.surfaceVariant, + highlightedColor: Theme.of(context).colorScheme.outline.withOpacity( + kHintOpacity, + ), + animationEnabled: false, + ), + ), + child: MultiSplitView( + controller: _controller, + children: [ + widget.leftWidget, + widget.rightWidget, + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/suggestions_menu.dart b/lib/widgets/suggestions_menu.dart new file mode 100644 index 00000000..a6ab063d --- /dev/null +++ b/lib/widgets/suggestions_menu.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:mention_tag_text_field/mention_tag_text_field.dart'; +import 'package:apidash/consts.dart'; + +class SuggestionsMenu extends StatelessWidget { + const SuggestionsMenu({ + super.key, + required this.mentionController, + required this.suggestions, + required this.suggestionBuilder, + this.menuWidth = kSuggestionsMenuWidth, + this.menuMaxHeight = kSuggestionsMenuMaxHeight, + }); + + final MentionTagTextEditingController mentionController; + final List? suggestions; + final double menuWidth; + final double menuMaxHeight; + final Widget? Function(BuildContext, int) suggestionBuilder; + + @override + Widget build(BuildContext context) { + return suggestions == null || suggestions!.isEmpty + ? const SizedBox.shrink() + : ClipRRect( + borderRadius: kBorderRadius8, + child: Material( + type: MaterialType.card, + elevation: 8, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: menuMaxHeight), + child: Ink( + width: menuWidth, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: kBorderRadius8, + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + child: ListView.separated( + shrinkWrap: true, + itemCount: suggestions?.length ?? 0, + separatorBuilder: (context, index) => + const Divider(height: 2), + itemBuilder: suggestionBuilder, + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/textfields.dart b/lib/widgets/textfields.dart deleted file mode 100644 index 7da215fc..00000000 --- a/lib/widgets/textfields.dart +++ /dev/null @@ -1,205 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:apidash/consts.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - -class URLField extends StatelessWidget { - const URLField({ - super.key, - required this.selectedId, - this.initialValue, - this.onChanged, - this.onFieldSubmitted, - }); - - final String selectedId; - final String? initialValue; - final void Function(String)? onChanged; - final void Function(String)? onFieldSubmitted; - - @override - Widget build(BuildContext context) { - return TextFormField( - key: Key("url-$selectedId"), - initialValue: initialValue, - style: kCodeStyle, - decoration: InputDecoration( - hintText: kHintTextUrlCard, - hintStyle: kCodeStyle.copyWith( - color: Theme.of(context).colorScheme.outline.withOpacity( - kHintOpacity, - ), - ), - border: InputBorder.none, - ), - onChanged: onChanged, - onFieldSubmitted: onFieldSubmitted, - onTapOutside: (PointerDownEvent event) { - FocusManager.instance.primaryFocus?.unfocus(); - }, - ); - } -} - -class CellField extends StatelessWidget { - const CellField({ - super.key, - required this.keyId, - this.initialValue, - this.hintText, - this.onChanged, - this.colorScheme, - }); - - final String keyId; - final String? initialValue; - final String? hintText; - final void Function(String)? onChanged; - final ColorScheme? colorScheme; - - @override - Widget build(BuildContext context) { - var clrScheme = colorScheme ?? Theme.of(context).colorScheme; - return TextFormField( - key: Key(keyId), - initialValue: initialValue, - style: kCodeStyle.copyWith( - color: clrScheme.onSurface, - ), - decoration: InputDecoration( - hintStyle: kCodeStyle.copyWith( - color: clrScheme.outline.withOpacity( - kHintOpacity, - ), - ), - hintText: hintText, - contentPadding: const EdgeInsets.only(bottom: 12), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: clrScheme.primary.withOpacity( - kHintOpacity, - ), - ), - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: clrScheme.surfaceVariant, - ), - ), - ), - onChanged: onChanged, - ); - } -} - -class ObscurableCellField extends HookWidget { - const ObscurableCellField({ - super.key, - required this.keyId, - this.initialValue, - this.hintText, - this.onChanged, - this.colorScheme, - }); - - final String keyId; - final String? initialValue; - final String? hintText; - final void Function(String)? onChanged; - final ColorScheme? colorScheme; - - @override - Widget build(BuildContext context) { - final obscureText = useState(true); - var clrScheme = colorScheme ?? Theme.of(context).colorScheme; - return TextFormField( - key: Key(keyId), - initialValue: initialValue, - style: kCodeStyle.copyWith( - color: clrScheme.onSurface, - ), - obscureText: obscureText.value, - decoration: InputDecoration( - hintStyle: kCodeStyle.copyWith( - color: clrScheme.outline.withOpacity( - kHintOpacity, - ), - ), - hintText: hintText, - suffixIcon: IconButton( - padding: kP4, - icon: Icon( - obscureText.value ? Icons.visibility : Icons.visibility_off, - color: clrScheme.onSurface, - size: 14, - ), - onPressed: () { - obscureText.value = !obscureText.value; - }, - ), - contentPadding: const EdgeInsets.only(bottom: 12), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: clrScheme.primary.withOpacity( - kHintOpacity, - ), - ), - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: clrScheme.surfaceVariant, - ), - ), - ), - onChanged: onChanged, - ); - } -} - -class JsonSearchField extends StatelessWidget { - const JsonSearchField({super.key, this.onChanged, this.controller}); - - final void Function(String)? onChanged; - final TextEditingController? controller; - - @override - Widget build(BuildContext context) { - return RawTextField( - controller: controller, - onChanged: onChanged, - style: kCodeStyle, - hintText: 'Search..', - ); - } -} - -class RawTextField extends StatelessWidget { - const RawTextField({ - super.key, - this.onChanged, - this.controller, - this.hintText, - this.style, - }); - - final void Function(String)? onChanged; - final TextEditingController? controller; - final String? hintText; - final TextStyle? style; - - @override - Widget build(BuildContext context) { - return TextField( - controller: controller, - onChanged: onChanged, - style: style, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: hintText, - ), - onTapOutside: (PointerDownEvent event) { - FocusManager.instance.primaryFocus?.unfocus(); - }, - ); - } -} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 8bcb12b6..ef7fc10d 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,14 +1,31 @@ -export 'buttons.dart'; -export 'cards.dart'; +export 'button_clear_response.dart'; +export 'button_copy.dart'; +export 'button_discord.dart'; +export 'button_repo.dart'; +export 'button_save_download.dart'; +export 'button_send.dart'; +export 'card_request_details.dart'; +export 'card_sidebar_environment.dart'; +export 'card_sidebar_request.dart'; export 'checkbox.dart'; export 'code_previewer.dart'; export 'codegen_previewer.dart'; -export 'dropdowns.dart'; +export 'dialogs.dart'; +export 'dropdown_codegen.dart'; +export 'dropdown_content_type.dart'; +export 'dropdown_formdata.dart'; +export 'dropdown_http_method.dart'; export 'editor_json.dart'; export 'editor.dart'; -export 'environment_field.dart'; +export 'environment_field_base.dart'; export 'error_message.dart'; -export 'headerfield.dart'; +export 'field_cell_obscurable.dart'; +export 'field_cell.dart'; +export 'field_header.dart'; +export 'field_json_search.dart'; +export 'field_mention.dart'; +export 'field_raw.dart'; +export 'field_url.dart'; export 'intro_message.dart'; export 'json_previewer.dart'; export 'markdown.dart'; @@ -19,10 +36,12 @@ export 'previewer.dart'; export 'request_widgets.dart'; export 'response_widgets.dart'; export 'snackbars.dart'; -export 'splitviews.dart'; +export 'splitview_drawer.dart'; +export 'splitview_dashboard.dart'; +export 'splitview_equal.dart'; +export 'suggestions_menu.dart'; export 'tables.dart'; export 'tabs.dart'; -export 'textfields.dart'; export 'texts.dart'; export 'uint8_audio_player.dart'; export 'window_caption.dart'; diff --git a/pubspec.lock b/pubspec.lock index de82d387..7973e264 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -821,10 +821,10 @@ packages: dependency: "direct main" description: name: mention_tag_text_field - sha256: b0e831f5fc8ca942a835916fd084e6f5054226715affc740c613cd320004b8a7 + sha256: "9768a0a6fe5771cb8eb98f94b26b4c595ca2487b0eb28b9d5624f8d71a2ac17a" url: "https://pub.dev" source: hosted - version: "0.0.4" + version: "0.0.5" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2b050d94..477f7f58 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: hooks_riverpod: ^2.5.1 flutter_hooks: ^0.20.5 flutter_portal: ^1.1.4 - mention_tag_text_field: ^0.0.4 + mention_tag_text_field: ^0.0.5 dependency_overrides: web: ^0.5.0 diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 94a326ef..b265058f 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -12,8 +12,7 @@ import 'package:apidash/screens/home_page/home_page.dart'; import 'package:apidash/screens/intro_page.dart'; import 'package:apidash/screens/settings_page.dart'; import 'package:apidash/services/hive_services.dart'; -import 'package:apidash/widgets/response_widgets.dart'; -import 'package:apidash/widgets/textfields.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -395,7 +394,7 @@ void main() { await tester.pump(); // Tap on the "Send" button - Finder sendButton = find.byType(SendButton); + Finder sendButton = find.byType(SendRequestButton); await tester.tap(sendButton); await tester.pump(); @@ -439,7 +438,7 @@ void main() { await tester.pump(); // Tap on the "Send" button - Finder sendButton = find.byType(SendButton); + Finder sendButton = find.byType(SendRequestButton); await tester.tap(sendButton); await tester.pump(); @@ -487,7 +486,7 @@ void main() { await tester.pump(); // Tap on the "Send" button - Finder sendButton = find.byType(SendButton); + Finder sendButton = find.byType(SendRequestButton); await tester.tap(sendButton); await tester.pump(); @@ -544,7 +543,7 @@ void main() { await tester.pump(); // Tap on the "Send" button - Finder sendButton = find.byType(SendButton); + Finder sendButton = find.byType(SendRequestButton); await tester.tap(sendButton); await tester.pump(); diff --git a/test/widgets/buttons_test.dart b/test/widgets/buttons_test.dart index 0e2a2171..31ff166e 100644 --- a/test/widgets/buttons_test.dart +++ b/test/widgets/buttons_test.dart @@ -1,8 +1,8 @@ import 'dart:typed_data'; +import 'package:apidash/screens/common_widgets/sidebar_save_button.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/buttons.dart'; -import 'package:apidash/screens/common/sidebar_widgets.dart'; import 'package:apidash/consts.dart'; import '../test_consts.dart'; @@ -36,7 +36,7 @@ void main() { title: 'Send Request button', theme: kThemeDataLight, home: Scaffold( - body: SendRequestButton( + body: SendButton( isWorking: false, onTap: () { changedValue = 'Send'; @@ -63,7 +63,7 @@ void main() { title: 'Send Request button', theme: kThemeDataLight, home: Scaffold( - body: SendRequestButton( + body: SendButton( isWorking: true, onTap: () {}, ), diff --git a/test/widgets/cards_test.dart b/test/widgets/cards_test.dart index ae62ef55..b66288eb 100644 --- a/test/widgets/cards_test.dart +++ b/test/widgets/cards_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/cards.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import '../test_consts.dart'; diff --git a/test/widgets/dropdowns_test.dart b/test/widgets/dropdowns_test.dart index 7d304ce8..cca255fc 100644 --- a/test/widgets/dropdowns_test.dart +++ b/test/widgets/dropdowns_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/dropdowns.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import '../test_consts.dart'; diff --git a/test/widgets/headerfield_test.dart b/test/widgets/headerfield_test.dart index 63a179f2..4475058b 100644 --- a/test/widgets/headerfield_test.dart +++ b/test/widgets/headerfield_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/headerfield.dart'; +import 'package:apidash/widgets/field_header.dart'; void main() { testWidgets('Testing Header Field', (tester) async { diff --git a/test/widgets/splitviews_test.dart b/test/widgets/splitviews_test.dart index 3e9d8d59..b8d074b1 100644 --- a/test/widgets/splitviews_test.dart +++ b/test/widgets/splitviews_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/splitviews.dart'; import 'package:multi_split_view/multi_split_view.dart'; +import 'package:apidash/widgets/widgets.dart'; void main() { testWidgets('Testing for Dashboard Splitview', (tester) async { diff --git a/test/widgets/textfields_test.dart b/test/widgets/textfields_test.dart index 88aa6392..6a83b8f7 100644 --- a/test/widgets/textfields_test.dart +++ b/test/widgets/textfields_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/textfields.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import '../test_consts.dart';