diff --git a/lib/consts.dart b/lib/consts.dart index cafcfff2..3f06d830 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -72,6 +72,7 @@ const kBorderRadius12 = BorderRadius.all(Radius.circular(12)); const kP1 = EdgeInsets.all(1); const kP5 = EdgeInsets.all(5); +const kP6 = EdgeInsets.all(6); const kP8 = EdgeInsets.all(8); const kPs8 = EdgeInsets.only(left: 8); const kPs2 = EdgeInsets.only(left: 2); @@ -95,6 +96,12 @@ const kPh20t40 = EdgeInsets.only( right: 20, top: 40, ); +const kPs0o6 = EdgeInsets.only( + left: 0, + top: 6, + right: 6, + bottom: 6, +); const kPh60 = EdgeInsets.symmetric(horizontal: 60); const kPh60v60 = EdgeInsets.symmetric(vertical: 60, horizontal: 60); const kP24CollectionPane = EdgeInsets.only( diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 5d929489..824c47d5 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -2,4 +2,11 @@ extension StringExtension on String { String capitalize() { return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; } + + String clip(int limit) { + if (length <= limit) { + return this; + } + return "${substring(0, limit)}..."; + } } diff --git a/lib/providers/environment_provider.dart b/lib/providers/environment_provider.dart index 355e2fe6..72566e22 100644 --- a/lib/providers/environment_provider.dart +++ b/lib/providers/environment_provider.dart @@ -8,6 +8,13 @@ import '../services/services.dart' show hiveHandler, HiveHandler; final selectedEnvironmentIdStateProvider = StateProvider((ref) => null); +final selectedEnvironmentModelProvider = + StateProvider((ref) { + final selectedId = ref.watch(selectedEnvironmentIdStateProvider); + final environments = ref.watch(environmentsStateNotifierProvider); + return selectedId != null ? environments![selectedId] : null; +}); + final StateNotifierProvider?> environmentsStateNotifierProvider = StateNotifierProvider((ref) => EnvironmentsStateNotifier(ref, hiveHandler)); diff --git a/lib/screens/common/main_editor_widgets.dart b/lib/screens/common/main_editor_widgets.dart new file mode 100644 index 00000000..4bb3c1aa --- /dev/null +++ b/lib/screens/common/main_editor_widgets.dart @@ -0,0 +1,132 @@ +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: DropdownButtonEnvironment( + activeEnvironment: environments?[activeEnvironment], + environments: environmentsList, + onChanged: (value) { + ref.read(activeEnvironmentIdStateProvider.notifier).state = value?.id; + }, + ), + ); + } +} + +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/envvar/environment_editor.dart b/lib/screens/envvar/environment_editor.dart new file mode 100644 index 00000000..89158f08 --- /dev/null +++ b/lib/screens/envvar/environment_editor.dart @@ -0,0 +1,37 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/screens/common/main_editor_widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class EnvironmentEditor extends ConsumerWidget { + const EnvironmentEditor({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: context.isMediumWindow + ? kPb10 + : (kIsMacOS || kIsWindows) + ? kPt24o8 + : kP8, + child: Column( + children: [ + kVSpacer5, + !context.isMediumWindow + ? const Padding( + padding: kPh4, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + EnvironmentDropdown(), + ], + ), + ) + : const SizedBox.shrink() + ], + ), + ); + } +} diff --git a/lib/screens/envvar/environment_page.dart b/lib/screens/envvar/environment_page.dart index aedf22b1..e84b15b0 100644 --- a/lib/screens/envvar/environment_page.dart +++ b/lib/screens/envvar/environment_page.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; +import '../common/main_editor_widgets.dart'; import 'environments_pane.dart'; +import 'environment_editor.dart'; class EnvironmentPage extends ConsumerWidget { const EnvironmentPage({ @@ -14,12 +18,43 @@ class EnvironmentPage extends ConsumerWidget { final GlobalKey scaffoldKey; @override Widget build(BuildContext context, WidgetRef ref) { + final id = ref.watch(selectedEnvironmentIdStateProvider); + final name = getEnvironmentTitle(ref.watch( + selectedEnvironmentModelProvider.select((value) => value?.name))); if (context.isMediumWindow) { return TwoDrawerScaffold( scaffoldKey: scaffoldKey, - mainContent: const SizedBox(), // TODO: replace placeholder - title: const Text("Environments"), // TODO: replace placeholder + mainContent: const EnvironmentEditor(), + title: ScaffoldTitle( + title: name, + showMenu: id != kGlobalEnvironmentId, + onSelected: (ItemMenuOption item) { + if (item == ItemMenuOption.edit) { + showRenameDialog(context, "Rename Environment", name, (val) { + ref + .read(environmentsStateNotifierProvider.notifier) + .updateEnvironment(id!, name: val); + }); + } + if (item == ItemMenuOption.delete) { + ref + .read(environmentsStateNotifierProvider.notifier) + .removeEnvironment(id!); + } + if (item == ItemMenuOption.duplicate) { + ref + .read(environmentsStateNotifierProvider.notifier) + .duplicateEnvironment(id!); + } + }, + ), leftDrawerContent: const EnvironmentsPane(), + actions: const [ + Padding( + padding: kPh8, + child: EnvironmentDropdown(), + ), + ], onDrawerChanged: (value) => ref.read(leftDrawerStateProvider.notifier).state = value, ); @@ -29,7 +64,7 @@ class EnvironmentPage extends ConsumerWidget { Expanded( child: DashboardSplitView( sidebarWidget: EnvironmentsPane(), - mainWidget: SizedBox(), + mainWidget: EnvironmentEditor(), ), ), ], diff --git a/lib/screens/home_page/editor_pane/editor_request.dart b/lib/screens/home_page/editor_pane/editor_request.dart index e035ab27..7087fc51 100644 --- a/lib/screens/home_page/editor_pane/editor_request.dart +++ b/lib/screens/home_page/editor_pane/editor_request.dart @@ -2,9 +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 'url_card.dart'; class RequestEditor extends StatelessWidget { @@ -57,7 +59,7 @@ class RequestEditorTopBar extends ConsumerWidget { padding: const EdgeInsets.only( left: 12.0, top: 4.0, - right: 8.0, + right: 4.0, bottom: 4.0, ), child: Row( @@ -73,71 +75,29 @@ class RequestEditorTopBar extends ConsumerWidget { const SizedBox( width: 6, ), - SizedBox( - width: 90, - height: 24, - child: FilledButton.tonalIcon( - style: const ButtonStyle( - padding: MaterialStatePropertyAll(EdgeInsets.zero), - ), - onPressed: () { - showRenameDialog(context, name, (val) { - ref - .read(collectionStateNotifierProvider.notifier) - .update(id!, name: val); - }); - }, - icon: const Icon( - Icons.edit, - size: 12, - ), - label: Text( - "Rename", - style: Theme.of(context).textTheme.bodySmall, - ), - ), - ) + OutlinedIconButton( + iconData: Icons.edit, + onPressed: () { + showRenameDialog(context, "Rename Request", name, (val) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(id!, name: val); + }); + }, + ), + kHSpacer4, + OutlinedIconButton( + onPressed: () { + ref + .read(environmentsStateNotifierProvider.notifier) + .duplicateEnvironment(id!); + }, + iconData: Icons.copy_outlined, + ), + kHSpacer10, + const EnvironmentDropdown(), ], ), ); } } - -showRenameDialog( - BuildContext context, - 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: const Text('Rename Request'), - 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/mobile/requests_page.dart b/lib/screens/mobile/requests_page.dart index f4a78156..94398175 100644 --- a/lib/screens/mobile/requests_page.dart +++ b/lib/screens/mobile/requests_page.dart @@ -5,10 +5,10 @@ import 'package:apidash/utils/http_utils.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/widgets/widgets.dart'; import '../home_page/collection_pane.dart'; -import '../home_page/editor_pane/editor_request.dart'; import '../home_page/editor_pane/editor_pane.dart'; import '../home_page/editor_pane/url_card.dart'; import '../home_page/editor_pane/details_card/code_pane.dart'; +import '../common/main_editor_widgets.dart'; import 'response_drawer.dart'; import 'widgets/page_base.dart'; @@ -22,9 +22,29 @@ class RequestsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final id = ref.watch(selectedIdStateProvider); + final name = getRequestTitleFromUrl( + ref.watch(selectedRequestModelProvider.select((value) => value?.name))); return TwoDrawerScaffold( scaffoldKey: scaffoldKey, - title: const RequestTitle(), + title: ScaffoldTitle( + title: name, + onSelected: (ItemMenuOption item) { + if (item == ItemMenuOption.edit) { + showRenameDialog(context, "Rename Request", name, (val) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(id!, name: val); + }); + } + if (item == ItemMenuOption.delete) { + ref.read(collectionStateNotifierProvider.notifier).remove(id!); + } + if (item == ItemMenuOption.duplicate) { + ref.read(collectionStateNotifierProvider.notifier).duplicate(id!); + } + }, + ), leftDrawerContent: const CollectionPane(), rightDrawerContent: const ResponseDrawer(), mainContent: const RequestEditorPane(), @@ -35,67 +55,6 @@ class RequestsPage extends ConsumerWidget { } } -class RequestTitle extends ConsumerWidget { - const RequestTitle({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final id = ref.watch(selectedIdStateProvider); - final name = getRequestTitleFromUrl( - ref.watch(selectedRequestModelProvider.select((value) => value?.name))); - return 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: name, - 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( - name, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, - maxLines: 1, - ), - ), - const Icon( - Icons.unfold_more_rounded, - size: 20, - ), - ], - ), - ), - onSelected: (ItemMenuOption item) { - if (item == ItemMenuOption.edit) { - showRenameDialog(context, name, (val) { - ref - .read(collectionStateNotifierProvider.notifier) - .update(id!, name: val); - }); - } - if (item == ItemMenuOption.delete) { - ref.read(collectionStateNotifierProvider.notifier).remove(id!); - } - if (item == ItemMenuOption.duplicate) { - ref.read(collectionStateNotifierProvider.notifier).duplicate(id!); - } - }, - ), - ), - ); - } -} - class RequestPageBottombar extends ConsumerWidget { const RequestPageBottombar({ super.key, diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart index e3b1d2d3..628d6418 100644 --- a/lib/widgets/buttons.dart +++ b/lib/widgets/buttons.dart @@ -232,3 +232,32 @@ class ClearResponseButton extends StatelessWidget { ); } } + +class OutlinedIconButton extends StatelessWidget { + const OutlinedIconButton({ + super.key, + required this.iconData, + this.onPressed, + this.size = 14, + }); + final double size; + final IconData iconData; + final void Function()? onPressed; + @override + Widget build(BuildContext context) { + return IconButton( + style: IconButton.styleFrom( + padding: kP6, + side: BorderSide( + color: Theme.of(context).colorScheme.outlineVariant, + ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + icon: Icon( + iconData, + size: size, + ), + onPressed: onPressed, + ); + } +} diff --git a/lib/widgets/dropdowns.dart b/lib/widgets/dropdowns.dart index 2d8a059b..d421beea 100644 --- a/lib/widgets/dropdowns.dart +++ b/lib/widgets/dropdowns.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/models/models.dart'; class DropdownButtonHttpMethod extends StatelessWidget { const DropdownButtonHttpMethod({ @@ -94,7 +95,7 @@ class DropdownButtonContentType extends StatelessWidget { } } -class DropdownButtonFormData extends StatefulWidget { +class DropdownButtonFormData extends StatelessWidget { const DropdownButtonFormData({ super.key, this.formDataType, @@ -104,18 +105,13 @@ class DropdownButtonFormData extends StatefulWidget { final FormDataType? formDataType; final void Function(FormDataType?)? onChanged; - @override - State createState() => _DropdownButtonFormData(); -} - -class _DropdownButtonFormData extends State { @override Widget build(BuildContext context) { final surfaceColor = Theme.of(context).colorScheme.surface; return DropdownButton( dropdownColor: surfaceColor, focusColor: surfaceColor, - value: widget.formDataType, + value: formDataType, icon: const Icon( Icons.unfold_more_rounded, size: 16, @@ -125,7 +121,7 @@ class _DropdownButtonFormData extends State { color: Theme.of(context).colorScheme.primary, ), underline: const IgnorePointer(), - onChanged: widget.onChanged, + onChanged: onChanged, borderRadius: kBorderRadius12, items: FormDataType.values .map>((FormDataType value) { @@ -192,3 +188,67 @@ class DropdownButtonCodegenLanguage extends StatelessWidget { ); } } + +class DropdownButtonEnvironment extends StatelessWidget { + const DropdownButtonEnvironment({ + super.key, + this.activeEnvironment, + this.onChanged, + this.environments, + }); + + final EnvironmentModel? activeEnvironment; + final void Function(EnvironmentModel? value)? onChanged; + final List? environments; + final EnvironmentModel? noneEnvironmentModel = null; + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + final characterLimit = context.isCompactWindow ? 12 : 15; + return DropdownButton( + isDense: true, + padding: kPs0o6, + focusColor: surfaceColor, + value: activeEnvironment, + icon: const Icon(Icons.unfold_more_rounded), + elevation: 4, + underline: Container( + height: 0, + ), + borderRadius: kBorderRadius8, + onChanged: onChanged, + items: [ + DropdownMenuItem( + value: noneEnvironmentModel, + child: Padding( + padding: EdgeInsets.only(left: context.isMediumWindow ? 8 : 16), + child: Text( + "No Environment", + style: kTextStyleButtonSmall.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ...environments?.map>( + (EnvironmentModel environmentModel) { + final name = getEnvironmentTitle(environmentModel.name); + return DropdownMenuItem( + value: environmentModel, + child: Padding( + padding: + EdgeInsets.only(left: context.isMediumWindow ? 8 : 16), + child: Text( + name.clip(characterLimit), + style: kTextStyleButtonSmall.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }).toList() ?? + [] + ], + ); + } +}