diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 248170b9..0a6be441 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -4,6 +4,7 @@ import 'package:inner_drawer/inner_drawer.dart'; final mobileDrawerKeyProvider = StateProvider>( (ref) => GlobalKey()); +final leftDrawerStateProvider = StateProvider((ref) => false); final navRailIndexStateProvider = StateProvider((ref) => 0); final selectedIdEditStateProvider = StateProvider((ref) => null); final codePaneVisibleStateProvider = StateProvider((ref) => false); @@ -29,4 +30,5 @@ final nameTextFieldFocusNodeProvider = return focusNode; }); -final searchQueryProvider = StateProvider((ref) => ''); +final collectionSearchQueryProvider = StateProvider((ref) => ''); +final environmentSearchQueryProvider = StateProvider((ref) => ''); diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 1c6aef0a..b5a9e033 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -58,11 +58,13 @@ class Dashboard extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.only(bottom: 16.0), + child: bottomButton(context, ref, railIdx, 2, child: bottomButton(context, ref, railIdx, 2, Icons.help, Icons.help_outline), ), Padding( padding: const EdgeInsets.only(bottom: 16.0), + child: bottomButton(context, ref, railIdx, 3, child: bottomButton(context, ref, railIdx, 3, Icons.settings, Icons.settings_outlined), ), @@ -95,6 +97,7 @@ class Dashboard extends ConsumerWidget { children: const [ HomePage(), SizedBox(), + SizedBox(), IntroPage(), SettingsPage(), ], diff --git a/lib/screens/envvar/environment_page.dart b/lib/screens/envvar/environment_page.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/screens/envvar/environments_pane.dart b/lib/screens/envvar/environments_pane.dart new file mode 100644 index 00000000..e5200889 --- /dev/null +++ b/lib/screens/envvar/environments_pane.dart @@ -0,0 +1,129 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/models/environment_model.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class EnvironmentsPane extends ConsumerWidget { + const EnvironmentsPane({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const SizedBox(); + } +} + +class EnvironmentsList extends HookConsumerWidget { + const EnvironmentsList({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final environmentSequence = ref.watch(environmentSequenceProvider); + final environmentItems = ref.watch(environmentsStateNotifierProvider)!; + final alwaysShowEnvironmentsPaneScrollbar = ref.watch(settingsProvider + .select((value) => value.alwaysShowCollectionPaneScrollbar)); + final filterQuery = ref.watch(environmentSearchQueryProvider).trim(); + + ScrollController scrollController = useScrollController(); + return Scrollbar( + controller: scrollController, + thumbVisibility: alwaysShowEnvironmentsPaneScrollbar, + radius: const Radius.circular(12), + child: filterQuery.isEmpty + ? ReorderableListView.builder( + padding: context.isMediumWindow + ? EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + right: 8, + ) + : kPe8, + scrollController: scrollController, + buildDefaultDragHandles: false, + itemCount: environmentSequence.length, + onReorder: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + if (oldIndex != newIndex) { + ref + .read(collectionStateNotifierProvider.notifier) + .reorder(oldIndex, newIndex); + } + }, + itemBuilder: (context, index) { + var id = environmentSequence[index]; + if (kIsMobile) { + return ReorderableDelayedDragStartListener( + key: ValueKey(id), + index: index, + child: Padding( + padding: kP1, + child: EnvironmentItem( + id: id, + environmentModel: environmentItems[id]!, + ), + ), + ); + } + return ReorderableDragStartListener( + key: ValueKey(id), + index: index, + child: Padding( + padding: kP1, + child: EnvironmentItem( + id: id, + environmentModel: environmentItems[id]!, + ), + ), + ); + }, + ) + : ListView( + padding: context.isMediumWindow + ? EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + right: 8, + ) + : kPe8, + controller: scrollController, + children: environmentSequence.map((id) { + var item = environmentItems[id]!; + if (item.name.toLowerCase().contains(filterQuery)) { + return Padding( + padding: kP1, + child: EnvironmentItem( + id: id, + environmentModel: item, + ), + ); + } + return const SizedBox(); + }).toList(), + ), + ); + } +} + +class EnvironmentItem extends ConsumerWidget { + const EnvironmentItem({ + super.key, + required this.id, + required this.environmentModel, + }); + + final String id; + final EnvironmentModel environmentModel; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedEnvironmentIdProvider); + + return Text(environmentModel.name); + } +} diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 91a7ed93..9d9b7340 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -94,7 +94,7 @@ class CollectionPane extends ConsumerWidget { style: Theme.of(context).textTheme.bodyMedium, hintText: "Filter by name or URL", onChanged: (value) { - ref.read(searchQueryProvider.notifier).state = + ref.read(collectionSearchQueryProvider.notifier).state = value.toLowerCase(); }, ), @@ -142,7 +142,7 @@ class _RequestListState extends ConsumerState { final requestItems = ref.watch(collectionStateNotifierProvider)!; final alwaysShowCollectionPaneScrollbar = ref.watch(settingsProvider .select((value) => value.alwaysShowCollectionPaneScrollbar)); - final filterQuery = ref.watch(searchQueryProvider).trim(); + final filterQuery = ref.watch(collectionSearchQueryProvider).trim(); return Scrollbar( controller: controller, diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart index acc4feec..123ccaea 100644 --- a/lib/screens/mobile/dashboard.dart +++ b/lib/screens/mobile/dashboard.dart @@ -1,4 +1,4 @@ -import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/splitviews.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -6,11 +6,13 @@ import 'package:inner_drawer/inner_drawer.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; +import '../intro_page.dart'; +import '../settings_page.dart'; import 'navbar.dart'; -import 'widgets/left_drawer.dart'; import 'requests_page.dart'; import 'response_drawer.dart'; import '../home_page/collection_pane.dart'; +import 'widgets/page_base.dart'; class MobileDashboard extends ConsumerStatefulWidget { const MobileDashboard({super.key}); @@ -20,31 +22,14 @@ class MobileDashboard extends ConsumerStatefulWidget { } class _MobileDashboardState extends ConsumerState { - late Color backgroundColor; - bool isLeftDrawerOpen = false; - ValueNotifier dragPosition = ValueNotifier(0); - ValueNotifier drawerDirection = - ValueNotifier(InnerDrawerDirection.start); - - Color calculateBackgroundColor(double dragPosition) { - Color start = Theme.of(context).colorScheme.surface; - Color end = Theme.of(context).colorScheme.onInverseSurface; - return dragPosition == 0 ? start : end; - } - - @override - void dispose() { - super.dispose(); - dragPosition.dispose(); - drawerDirection.dispose(); - } - @override Widget build( BuildContext context, ) { + final railIdx = ref.watch(navRailIndexStateProvider); final GlobalKey innerDrawerKey = ref.watch(mobileDrawerKeyProvider); + final isLeftDrawerOpen = ref.watch(leftDrawerStateProvider); return AnnotatedRegion( value: FlexColorScheme.themedSystemNavigationBar( context, @@ -54,65 +39,9 @@ class _MobileDashboardState extends ConsumerState { child: Stack( alignment: AlignmentDirectional.bottomCenter, children: [ - InnerDrawer( - key: innerDrawerKey, - swipe: true, - swipeChild: true, - onTapClose: true, - offset: !context.isCompactWindow - ? const IDOffset.only(left: 0.1, right: 1) - : const IDOffset.only(left: 0.7, right: 1), - boxShadow: [ - BoxShadow( - offset: const Offset(1, 0), - color: Theme.of(context).colorScheme.onInverseSurface, - blurRadius: 0, - ), - ], - colorTransitionChild: Colors.transparent, - colorTransitionScaffold: Colors.transparent, - rightAnimationType: InnerDrawerAnimation.linear, - backgroundDecoration: BoxDecoration( - color: Theme.of(context).colorScheme.onInverseSurface), - onDragUpdate: (value, direction) { - drawerDirection.value = direction; - if (value > 0.98 && direction == InnerDrawerDirection.start) { - dragPosition.value = 1; - } else { - dragPosition.value = 0; - } - }, - innerDrawerCallback: (isOpened) { - if (drawerDirection.value == InnerDrawerDirection.start) { - setState(() { - isLeftDrawerOpen = isOpened; - }); - } - }, - leftChild: const LeftDrawer( - drawerContent: CollectionPane(), - ), - rightChild: const ResponseDrawer(), - scaffold: ValueListenableBuilder( - valueListenable: dragPosition, - builder: (context, value, child) { - return Container( - color: calculateBackgroundColor(value), - child: child, - ); - }, - child: ClipRRect( - borderRadius: - const BorderRadius.only(topLeft: Radius.circular(8)), - child: SafeArea( - minimum: kIsWindows || kIsMacOS ? kPt28 : EdgeInsets.zero, - bottom: false, - child: RequestsPage( - innerDrawerKey: innerDrawerKey, - ), - ), - ), - ), + PageBranch( + pageIndex: railIdx, + innerDrawerKey: innerDrawerKey, ), if (context.isCompactWindow) AnimatedPositioned( @@ -131,3 +60,53 @@ class _MobileDashboardState extends ConsumerState { ); } } + +class PageBranch extends StatelessWidget { + const PageBranch({ + super.key, + required this.pageIndex, + required this.innerDrawerKey, + }); + + final int pageIndex; + final GlobalKey innerDrawerKey; + + @override + Widget build(BuildContext context) { + switch (pageIndex) { + case 1: + return TwoDrawerSplitView( + key: const ValueKey('env'), + innerDrawerKey: innerDrawerKey, + offset: !context.isCompactWindow + ? const IDOffset.only(left: 0.1) + : const IDOffset.only(left: 0.7), + leftDrawerContent: const SizedBox(), + mainContent: const SizedBox(), + ); + case 2: + return const PageBase( + title: 'About', + scaffoldBody: IntroPage(), + ); + case 3: + return const PageBase( + title: 'Settings', + scaffoldBody: SettingsPage(), + ); + default: + return TwoDrawerSplitView( + key: const ValueKey('home'), + innerDrawerKey: innerDrawerKey, + offset: !context.isCompactWindow + ? const IDOffset.only(left: 0.1, right: 1) + : const IDOffset.only(left: 0.7, right: 1), + leftDrawerContent: const CollectionPane(), + rightDrawerContent: const ResponseDrawer(), + mainContent: RequestsPage( + innerDrawerKey: innerDrawerKey, + ), + ); + } + } +} diff --git a/lib/screens/mobile/navbar.dart b/lib/screens/mobile/navbar.dart index 5509152d..ab516c5b 100644 --- a/lib/screens/mobile/navbar.dart +++ b/lib/screens/mobile/navbar.dart @@ -2,9 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/extensions/context_extensions.dart'; import 'package:apidash/providers/ui_providers.dart'; -import 'package:apidash/screens/mobile/widgets/page_base.dart'; -import '../settings_page.dart'; -import '../intro_page.dart'; class BottomNavBar extends ConsumerWidget { const BottomNavBar({super.key}); @@ -48,30 +45,26 @@ class BottomNavBar extends ConsumerWidget { 'Variables'), ), Expanded( - child: customNavigationDestination(context, ref, railIdx, 2, - Icons.help, Icons.help_outline, 'About', - isNavigator: true, onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const PageBase( - title: 'About', - scaffoldBody: IntroPage(), - )), - ); - }), + child: customNavigationDestination( + context, + ref, + railIdx, + 2, + Icons.help, + Icons.help_outline, + 'About', + ), ), Expanded( - child: customNavigationDestination(context, ref, railIdx, 3, - Icons.settings, Icons.settings_outlined, 'Settings', - isNavigator: true, onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const PageBase( - title: 'Settings', - scaffoldBody: SettingsPage(), - )), - ); - }), + child: customNavigationDestination( + context, + ref, + railIdx, + 3, + Icons.settings, + Icons.settings_outlined, + 'Settings', + ), ), ], ), @@ -131,17 +124,6 @@ class NavRail extends ConsumerWidget { Icons.help, Icons.help_outline, 'About', - isNavigator: true, - showLabel: false, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const PageBase( - title: 'About', - scaffoldBody: IntroPage(), - )), - ); - }, ), const SizedBox(height: 24), customNavigationDestination( @@ -152,17 +134,6 @@ class NavRail extends ConsumerWidget { Icons.settings, Icons.settings_outlined, 'Settings', - isNavigator: true, - showLabel: false, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const PageBase( - title: 'Settings', - scaffoldBody: SettingsPage(), - )), - ); - }, ), ], ), @@ -179,7 +150,6 @@ Widget customNavigationDestination( IconData selectedIcon, IconData icon, String label, { - bool isNavigator = false, bool showLabel = true, Function()? onTap, }) { @@ -195,9 +165,9 @@ Widget customNavigationDestination( onTap: isSelected ? null : () { - if (!isNavigator) { - ref.read(navRailIndexStateProvider.notifier).state = - buttonIdx; + ref.read(navRailIndexStateProvider.notifier).state = buttonIdx; + if (railIdx > 1 && buttonIdx <= 1) { + ref.read(leftDrawerStateProvider.notifier).state = false; } onTap?.call(); }, @@ -218,9 +188,11 @@ Widget customNavigationDestination( onTap: isSelected ? null : () { - if (!isNavigator) { - ref.read(navRailIndexStateProvider.notifier).state = - buttonIdx; + ref.read(navRailIndexStateProvider.notifier).state = + buttonIdx; + if (railIdx > 1 && buttonIdx <= 1) { + ref.read(leftDrawerStateProvider.notifier).state = + false; } onTap?.call(); }, diff --git a/lib/screens/mobile/widgets/page_base.dart b/lib/screens/mobile/widgets/page_base.dart index 39cc7156..42460831 100644 --- a/lib/screens/mobile/widgets/page_base.dart +++ b/lib/screens/mobile/widgets/page_base.dart @@ -16,7 +16,8 @@ class PageBase extends ConsumerWidget { return Stack( children: [ Container( - padding: kIsWindows || kIsMacOS ? kPt28 : EdgeInsets.zero, + padding: const EdgeInsets.only(bottom: 70) + + (kIsWindows || kIsMacOS ? kPt28 : EdgeInsets.zero), color: Theme.of(context).colorScheme.surface, child: Scaffold( backgroundColor: Theme.of(context).colorScheme.background, diff --git a/lib/widgets/cards.dart b/lib/widgets/cards.dart index fd99d492..5983608d 100644 --- a/lib/widgets/cards.dart +++ b/lib/widgets/cards.dart @@ -152,3 +152,41 @@ class RequestDetailsCard extends StatelessWidget { ); } } + +class SidebarEnvironmentCard extends StatelessWidget { + const SidebarEnvironmentCard({ + super.key, + required this.id, + this.isGlobal = false, + this.isSelected = false, + this.isActive = false, + this.name, + this.editRequestId, + this.onTap, + this.onDoubleTap, + this.onSecondaryTap, + this.onChangedNameEditor, + this.focusNode, + this.onTapOutsideNameEditor, + this.onMenuSelected, + }); + + final String id; + final bool isGlobal; + final bool isSelected; + final bool isActive; + final String? name; + final String? editRequestId; + final void Function()? onTap; + final void Function()? onDoubleTap; + final void Function()? onSecondaryTap; + final Function(String)? onChangedNameEditor; + final FocusNode? focusNode; + final Function()? onTapOutsideNameEditor; + final Function(RequestItemMenuOption)? onMenuSelected; + + @override + Widget build(BuildContext context) { + return const SizedBox(); + } +} diff --git a/lib/widgets/splitviews.dart b/lib/widgets/splitviews.dart index 935bc617..f4a30e32 100644 --- a/lib/widgets/splitviews.dart +++ b/lib/widgets/splitviews.dart @@ -1,6 +1,11 @@ +import 'package:apidash/screens/mobile/widgets/left_drawer.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:inner_drawer/inner_drawer.dart'; import 'package:multi_split_view/multi_split_view.dart'; import 'package:apidash/consts.dart'; +import 'package:apidash/providers/ui_providers.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; class DashboardSplitView extends StatefulWidget { const DashboardSplitView({ @@ -110,3 +115,85 @@ class _EqualSplitViewState extends State { super.dispose(); } } + +class TwoDrawerSplitView extends HookConsumerWidget { + const TwoDrawerSplitView({ + super.key, + required this.innerDrawerKey, + required this.offset, + required this.mainContent, + required this.leftDrawerContent, + this.rightDrawerContent, + }); + + final GlobalKey innerDrawerKey; + final IDOffset offset; + final Widget mainContent; + final Widget leftDrawerContent; + final Widget? rightDrawerContent; + + @override + Widget build(BuildContext context, WidgetRef ref) { + ValueNotifier dragPosition = useState(0.0); + ValueNotifier drawerDirection = + ValueNotifier(InnerDrawerDirection.start); + + Color calculateBackgroundColor(double dragPosition) { + Color start = Theme.of(context).colorScheme.surface; + Color end = Theme.of(context).colorScheme.onInverseSurface; + return dragPosition == 0 ? start : end; + } + + return InnerDrawer( + key: innerDrawerKey, + swipe: true, + swipeChild: true, + onTapClose: true, + offset: offset, + boxShadow: [ + BoxShadow( + offset: const Offset(1, 0), + color: Theme.of(context).colorScheme.onInverseSurface, + blurRadius: 0, + ), + ], + colorTransitionChild: Colors.transparent, + colorTransitionScaffold: Colors.transparent, + rightAnimationType: InnerDrawerAnimation.linear, + backgroundDecoration: + BoxDecoration(color: Theme.of(context).colorScheme.onInverseSurface), + onDragUpdate: (value, direction) { + drawerDirection.value = direction; + if (value > 0.98 && direction == InnerDrawerDirection.start) { + dragPosition.value = 1; + } else { + dragPosition.value = 0; + } + }, + innerDrawerCallback: (isOpened) { + if (drawerDirection.value == InnerDrawerDirection.start) { + ref.read(leftDrawerStateProvider.notifier).state = isOpened; + } + }, + leftChild: LeftDrawer(drawerContent: leftDrawerContent), + rightChild: rightDrawerContent, + scaffold: ValueListenableBuilder( + valueListenable: dragPosition, + builder: (context, value, child) { + return Container( + color: calculateBackgroundColor(value), + child: child, + ); + }, + child: ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(8)), + child: SafeArea( + minimum: kIsWindows || kIsMacOS ? kPt28 : EdgeInsets.zero, + bottom: false, + child: mainContent, + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0d8cedef..bd3a0f36 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -390,6 +390,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" flutter_keyboard_visibility: dependency: transitive description: @@ -576,6 +584,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" + url: "https://pub.dev" + source: hosted + version: "2.5.1" html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 88294a6e..3d092341 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,8 @@ dependencies: flex_color_scheme: ^7.3.1 data_table_2: ^2.5.11 file_selector: ^1.0.3 + hooks_riverpod: ^2.5.1 + flutter_hooks: ^0.20.5 dependency_overrides: web: ^0.5.0