diff --git a/lib/consts.dart b/lib/consts.dart index cb679187..064f9ffa 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -115,18 +115,24 @@ const kPv8 = EdgeInsets.symmetric(vertical: 8); const kPv10 = EdgeInsets.symmetric(vertical: 10); const kPv20 = EdgeInsets.symmetric(vertical: 20); const kPh2 = EdgeInsets.symmetric(horizontal: 2); +const kPt24o8 = EdgeInsets.only(top: 24, left: 8.0, right: 8.0, bottom: 8.0); +const kPt24 = EdgeInsets.only(top: 24); +const kPt8 = EdgeInsets.only(top: 8); const kPt28o8 = EdgeInsets.only(top: 28, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 10.0); const kPh4 = EdgeInsets.symmetric(horizontal: 4); const kPh8 = EdgeInsets.symmetric(horizontal: 8); +const kPh20 = EdgeInsets.symmetric( + horizontal: 20, +); +// const kPh20t3 = EdgeInsets.only( const kPh12 = EdgeInsets.symmetric(horizontal: 12); -const kPh20 = EdgeInsets.symmetric(horizontal: 20); const kPh24 = EdgeInsets.symmetric(horizontal: 24); const kPh20t40 = EdgeInsets.only( left: 20, right: 20, - top: 40, + top: 3 ); const kPs0o6 = EdgeInsets.only( left: 0, @@ -138,9 +144,7 @@ const kPh60 = EdgeInsets.symmetric(horizontal: 60); const kPh60v60 = EdgeInsets.symmetric(vertical: 60, horizontal: 60); const kP24CollectionPane = EdgeInsets.only( top: 24, - left: 4.0, - //right: 4.0, - // bottom: 8.0, + left: 8.0, ); const kP8CollectionPane = EdgeInsets.only( top: 8.0, @@ -148,15 +152,9 @@ const kP8CollectionPane = EdgeInsets.only( //right: 4.0, // bottom: 8.0, ); -const kPt8 = EdgeInsets.only( - top: 8, -); const kPt20 = EdgeInsets.only( top: 20, ); -const kPt24 = EdgeInsets.only( - top: 24, -); const kPt28 = EdgeInsets.only( top: 28, ); diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 02eead40..0b4d169b 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -24,6 +24,11 @@ final requestSequenceProvider = StateProvider>((ref) { return ids ?? []; }); +final requestTabSequenceProvider = StateProvider>((ref) { + var ids = hiveHandler.getTabIds(); + return ids ?? []; +}); + final StateNotifierProvider?> collectionStateNotifierProvider = StateNotifierProvider((ref) => CollectionStateNotifier(ref, hiveHandler)); @@ -37,6 +42,9 @@ class CollectionStateNotifier ref.read(requestSequenceProvider.notifier).state = [ state!.keys.first, ]; + ref.read(requestTabSequenceProvider.notifier).state = [ + state!.keys.first, + ]; } ref.read(selectedIdStateProvider.notifier).state = ref.read(requestSequenceProvider)[0]; @@ -81,10 +89,21 @@ class CollectionStateNotifier ref .read(requestSequenceProvider.notifier) .update((state) => [id, ...state]); + ref + .read(requestTabSequenceProvider.notifier) + .update((state) => [id, ...state]); + ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id; ref.read(hasUnsavedChangesProvider.notifier).state = true; } + void reorderTab(int oldIdx, int newIdx){ + var itemIds = ref.read(requestTabSequenceProvider); + final itemId = itemIds.removeAt(oldIdx); + itemIds.insert(newIdx, itemId); + ref.read(requestTabSequenceProvider.notifier).state = [...itemIds]; + } + void reorder(int oldIdx, int newIdx) { var itemIds = ref.read(requestSequenceProvider); final itemId = itemIds.removeAt(oldIdx); @@ -108,6 +127,10 @@ class CollectionStateNotifier newId = null; } + final tabs = ref.read(requestTabSequenceProvider); + tabs.remove(id); + ref.read(requestTabSequenceProvider.notifier).state = [...tabs]; + ref.read(selectedIdStateProvider.notifier).state = newId; var map = {...state!}; @@ -353,7 +376,9 @@ class CollectionStateNotifier ref.read(saveDataStateProvider.notifier).state = true; final saveResponse = ref.read(settingsProvider).saveResponses; final ids = ref.read(requestSequenceProvider); + final tabIds = ref.read(requestTabSequenceProvider); await hiveHandler.setIds(ids); + await hiveHandler.setTabIds(tabIds); for (var id in ids) { await hiveHandler.setRequestModel( id, diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index d5bce2f1..cafd30a7 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -1,12 +1,8 @@ +import 'package:apidash/providers/providers.dart'; 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'; -import 'common_widgets/common_widgets.dart'; -import 'envvar/environment_page.dart'; + import 'home_page/home_page.dart'; -import 'history/history_page.dart'; import 'settings_page.dart'; class Dashboard extends ConsumerWidget { @@ -17,112 +13,119 @@ class Dashboard extends ConsumerWidget { final railIdx = ref.watch(navRailIndexStateProvider); return Scaffold( body: SafeArea( - child: Row( - children: [ - Column( - children: [ - SizedBox( - height: kIsMacOS ? 32.0 : 16.0, - width: 64, - ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - isSelected: railIdx == 0, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 0; - }, - icon: const Icon(Icons.auto_awesome_mosaic_outlined), - selectedIcon: const Icon(Icons.auto_awesome_mosaic), - ), - Text( - 'Requests', - style: Theme.of(context).textTheme.labelSmall, - ), - kVSpacer10, - IconButton( - isSelected: railIdx == 1, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 1; - }, - icon: const Icon(Icons.laptop_windows_outlined), - selectedIcon: const Icon(Icons.laptop_windows), - ), - Text( - 'Variables', - style: Theme.of(context).textTheme.labelSmall, - ), - kVSpacer10, - IconButton( - isSelected: railIdx == 2, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 2; - }, - icon: const Icon(Icons.history_outlined), - selectedIcon: const Icon(Icons.history_rounded), - ), - Text( - 'History', - style: Theme.of(context).textTheme.labelSmall, - ), - ], + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: MediaQuery.of(context).size.width, + height: 30, + color: Theme.of(context).colorScheme.surfaceVariant, + alignment: Alignment.center, + child: const Text( + 'API Dash', + style: TextStyle( + fontWeight: FontWeight.bold, ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: NavbarButton( - railIdx: railIdx, - selectedIcon: Icons.help, - icon: Icons.help_outline, - label: 'About', - showLabel: false, - isCompact: true, - onTap: () { - showAboutAppDialog(context); - }, + ), + ), + Expanded( + child: Row( + children: [ + SizedBox( + width: 64, + child: Column( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + isSelected: railIdx == 0, + onPressed: () { + ref + .read(navRailIndexStateProvider.notifier) + .state = 0; + }, + icon: const Icon( + Icons.auto_awesome_mosaic_outlined), + selectedIcon: + const Icon(Icons.auto_awesome_mosaic), + ), + Text( + 'Requests', + style: Theme.of(context).textTheme.labelSmall, + ), + ], ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: NavbarButton( - railIdx: railIdx, - buttonIdx: 3, - selectedIcon: Icons.settings, - icon: Icons.settings_outlined, - label: 'Settings', - showLabel: false, - isCompact: true, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: bottomButton(context, ref, railIdx, 1, + Icons.help, Icons.help_outline), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: bottomButton(context, ref, railIdx, 2, + Icons.settings, Icons.settings_outlined), + ), + ], + ), ), - ), - ], + ], + ), ), - ), - ], - ), - VerticalDivider( - thickness: 1, - width: 1, - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - Expanded( - child: IndexedStack( - alignment: AlignmentDirectional.topCenter, - index: railIdx, - children: const [ - HomePage(), - EnvironmentPage(), - HistoryPage(), - SettingsPage(), + VerticalDivider( + thickness: 1, + width: 1, + color: Theme.of(context).colorScheme.surfaceVariant, + ), + Expanded( + child: IndexedStack( + alignment: AlignmentDirectional.topCenter, + index: railIdx, + children: const [ + HomePage(), + SettingsPage(), + ], + ), + ) ], ), - ) + ), ], ), ), ); } + + TextButton bottomButton( + BuildContext context, + WidgetRef ref, + int railIdx, + int buttonIdx, + IconData selectedIcon, + IconData icon, + ) { + bool isSelected = railIdx == buttonIdx; + return TextButton( + style: isSelected + ? TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + ) + : null, + onPressed: isSelected + ? null + : () { + ref.read(navRailIndexStateProvider.notifier).state = buttonIdx; + }, + child: Icon( + isSelected ? selectedIcon : icon, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + } } diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index b30eded2..a23f207d 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -24,10 +24,7 @@ class CollectionPane extends ConsumerWidget { ); } return Padding( - padding: (!context.isMediumWindow && kIsMacOS - ? kP24CollectionPane - : kP8CollectionPane) + - (context.isMediumWindow ? kPb70 : EdgeInsets.zero), + padding: kP8CollectionPane, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -221,6 +218,11 @@ class RequestItem extends ConsumerWidget { editRequestId: editRequestId, onTap: () { ref.read(selectedIdStateProvider.notifier).state = id; + final tabs = ref.read(requestTabSequenceProvider); + if(!tabs.contains(id)){ + tabs.add(id); + ref.read(requestTabSequenceProvider.notifier).state = [...tabs]; + } kHomeScaffoldKey.currentState?.closeDrawer(); }, // onDoubleTap: () { diff --git a/lib/screens/home_page/editor_pane/editor_pane.dart b/lib/screens/home_page/editor_pane/editor_pane.dart index 7ff67f5c..3e373ba8 100644 --- a/lib/screens/home_page/editor_pane/editor_pane.dart +++ b/lib/screens/home_page/editor_pane/editor_pane.dart @@ -1,6 +1,9 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:apidash/providers/providers.dart'; + import 'editor_default.dart'; import 'editor_request.dart'; @@ -15,7 +18,17 @@ class RequestEditorPane extends ConsumerWidget { if (selectedId == null) { return const RequestEditorDefault(); } else { - return const RequestEditor(); + return const Column( + children: [ + RequestTabView(), + Expanded( + child: Padding( + padding: kP8, + child: RequestEditor(), + ), + ), + ], + ); } } } diff --git a/lib/screens/home_page/editor_pane/editor_request.dart b/lib/screens/home_page/editor_pane/editor_request.dart index 52f49a3f..d9fdc626 100644 --- a/lib/screens/home_page/editor_pane/editor_request.dart +++ b/lib/screens/home_page/editor_pane/editor_request.dart @@ -1,12 +1,14 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/models/request_model.dart'; +import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/consts.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../common_widgets/common_widgets.dart'; import 'details_card/details_card.dart'; import 'details_card/request_pane/request_pane.dart'; -import '../../common_widgets/common_widgets.dart'; import 'url_card.dart'; class RequestEditor extends StatelessWidget { @@ -42,6 +44,141 @@ class RequestEditor extends StatelessWidget { } } +class RequestTabView extends ConsumerStatefulWidget { + const RequestTabView({super.key}); + + @override + ConsumerState createState() => _RequestTabViewState(); +} + +class _RequestTabViewState extends ConsumerState { + late final ScrollController controller; + + @override + void initState() { + super.initState(); + controller = ScrollController(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build( + BuildContext context, + ) { + final tabSequence = ref.watch(requestTabSequenceProvider); + final requestItems = ref.watch(collectionStateNotifierProvider)!; + final alwaysShowCollectionPaneScrollbar = ref.watch(settingsProvider + .select((value) => value.alwaysShowCollectionPaneScrollbar)); + + return Scrollbar( + controller: controller, + thumbVisibility: alwaysShowCollectionPaneScrollbar ? true : null, + thickness: 5, + radius: const Radius.circular(12), + child: Container( + width: MediaQuery.of(context).size.width, + constraints: const BoxConstraints( + maxHeight: 42, + minHeight: 42, + ), + padding: const EdgeInsets.only(bottom: 7), + child: ReorderableListView.builder( + physics: const BouncingScrollPhysics(), + shrinkWrap: true, + scrollController: controller, + scrollDirection: Axis.horizontal, + buildDefaultDragHandles: false, + itemCount: tabSequence.length, + onReorder: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + if (oldIndex != newIndex) { + ref + .read(collectionStateNotifierProvider.notifier) + .reorderTab(oldIndex, newIndex); + } + }, + itemExtentBuilder: (index, dimensions) { + final length = + dimensions.viewportMainAxisExtent / tabSequence.length; + + if (length > dimensions.viewportMainAxisExtent / 6) { + return dimensions.viewportMainAxisExtent / 6; + } else if (length < dimensions.viewportMainAxisExtent / 8) { + return dimensions.viewportMainAxisExtent / 8; + } else { + return length; + } + }, + itemBuilder: (context, index) { + var id = tabSequence[index]; + return ReorderableDragStartListener( + key: ValueKey(id), + index: index, + child: TabItem( + id: id, + requestModel: requestItems[id]!, + ), + ); + }, + ), + ), + ); + } +} + +class TabItem extends ConsumerWidget { + const TabItem({ + super.key, + required this.id, + required this.requestModel, + }); + + final String id; + final RequestModel requestModel; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final editRequestId = ref.watch(selectedIdEditStateProvider); + + return TabRequestCard( + id: id, + method: requestModel.httpRequestModel!.method, + name: requestModel.name, + url: requestModel.httpRequestModel!.url, + selectedId: selectedId, + editRequestId: editRequestId, + onTap: () { + ref.read(selectedIdStateProvider.notifier).state = id; + }, + onClose: () { + final tabs = ref.read(requestTabSequenceProvider); + final idx = tabs.indexOf(id); + tabs.remove(id); + ref.read(requestTabSequenceProvider.notifier).state = [...tabs]; + + String? newId; + if (idx > 0) { + newId = tabs[idx - 1]; + } else if (tabs.isNotEmpty) { + newId = tabs.first; + } else { + newId = null; + } + + ref.read(selectedIdStateProvider.notifier).state = newId; + }, + ); + } +} + class RequestEditorTopBar extends ConsumerWidget { const RequestEditorTopBar({super.key}); @@ -53,8 +190,8 @@ class RequestEditorTopBar extends ConsumerWidget { return Padding( padding: const EdgeInsets.only( left: 12.0, + right: 8.0, top: 4.0, - right: 4.0, bottom: 4.0, ), child: Row( diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 09ac8489..30abad78 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -3,6 +3,7 @@ import 'package:hive_flutter/hive_flutter.dart'; const String kDataBox = "apidash-data"; const String kKeyDataBoxIds = "ids"; +const String kKeyDataBoxTabIds = "tab-ids"; const String kEnvironmentBox = "apidash-environments"; const String kKeyEnvironmentBoxIds = "environmentIds"; @@ -103,6 +104,9 @@ class HiveHandler { dynamic getIds() => dataBox.get(kKeyDataBoxIds); Future setIds(List? ids) => dataBox.put(kKeyDataBoxIds, ids); + dynamic getTabIds() => dataBox.get(kKeyDataBoxTabIds); + Future setTabIds(List? ids) => dataBox.put(kKeyDataBoxTabIds, ids); + dynamic getRequestModel(String id) => dataBox.get(id); Future setRequestModel( String id, Map? requestModelJson) => diff --git a/lib/widgets/card_sidebar_request.dart b/lib/widgets/card_sidebar_request.dart index ab8a4706..edd326a8 100644 --- a/lib/widgets/card_sidebar_request.dart +++ b/lib/widgets/card_sidebar_request.dart @@ -33,6 +33,7 @@ class SidebarRequestCard extends StatelessWidget { final void Function()? onDoubleTap; final void Function()? onSecondaryTap; final Function(String)? onChangedNameEditor; + // final TextEditingController? controller; final FocusNode? focusNode; final Function()? onTapOutsideNameEditor; @@ -132,3 +133,102 @@ class SidebarRequestCard extends StatelessWidget { ); } } + +class TabRequestCard extends StatelessWidget { + const TabRequestCard({ + super.key, + required this.id, + required this.method, + this.name, + this.url, + this.selectedId, + this.editRequestId, + this.onTap, + this.onClose, + }); + + final String id; + final String? name; + final String? url; + final HTTPVerb method; + final String? selectedId; + final String? editRequestId; + final void Function()? onTap; + final void Function()? onClose; + + @override + Widget build(BuildContext context) { + final Color color = Theme.of(context).colorScheme.surface; + final Color colorVariant = + Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5); + final Color surfaceTint = Theme.of(context).colorScheme.primary; + bool isSelected = selectedId == id; + bool inEditMode = editRequestId == id; + String nm = (name != null && name!.trim().isNotEmpty) + ? name! + : getRequestTitleFromUrl(url); + return Tooltip( + message: nm, + waitDuration: const Duration(seconds: 1), + child: Card( + // shape: const RoundedRectangleBorder( + // borderRadius: BorderRadius.only( + // topLeft: Radius.circular(8), + // topRight: Radius.circular(8), + // ), + // ), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, + ), + elevation: isSelected ? 1 : 0, + surfaceTintColor: isSelected ? surfaceTint : null, + color: isSelected + ? Theme.of(context).colorScheme.brightness == Brightness.dark + ? colorVariant + : color + : color, + margin: EdgeInsets.zero, + child: InkWell( + // borderRadius: const BorderRadius.only( + // topLeft: Radius.circular(8), + // topRight: Radius.circular(8), + // ), + borderRadius: BorderRadius.zero, + hoverColor: colorVariant, + focusColor: colorVariant.withOpacity(0.5), + onTap: inEditMode ? null : onTap, + child: Container( + padding: EdgeInsets.only( + left: 6, + right: isSelected ? 6 : 10, + top: 2, + bottom: 2, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + MethodBox(method: method), + kHSpacer4, + Expanded( + child: Text( + nm, + softWrap: false, + overflow: TextOverflow.fade, + ), + ), + kHSpacer4, + GestureDetector( + onTap: onClose, + child: const Icon( + Icons.close, + size: 12, + ), + ), + ], + ), + ), + ), + ), + ); + } +}