Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Tabs at the top of the request pane #676

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions lib/providers/collection_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,35 @@ final StateNotifierProvider<CollectionStateNotifier, Map<String, RequestModel>?>
hiveHandler,
));

// Manages the set of visible tab IDs
final visibleTabsProvider = StateNotifierProvider<VisibleTabsNotifier, Set<String>>((ref) {
return VisibleTabsNotifier(ref);
});

class VisibleTabsNotifier extends StateNotifier<Set<String>> {
VisibleTabsNotifier(this.ref) : super({});

final Ref ref;

// Toggles a tab’s visibility in the tab bar
void toggleVisibility(String id) {
state = Set.from(state);
if (state.contains(id)) {
state.remove(id); // Hide the tab
} else {
state.add(id); // Show the tab (if reopened)
}
}

void showAll() {
state = ref.read(requestSequenceProvider).toSet();
}

void addSingleTab(String id) {
state = {id};
}
}

class CollectionStateNotifier
extends StateNotifier<Map<String, RequestModel>?> {
CollectionStateNotifier(
Expand All @@ -45,8 +74,6 @@ class CollectionStateNotifier
state!.keys.first,
];
}
ref.read(selectedIdStateProvider.notifier).state =
ref.read(requestSequenceProvider)[0];
});
}

Expand Down Expand Up @@ -77,6 +104,7 @@ class CollectionStateNotifier
.read(requestSequenceProvider.notifier)
.update((state) => [id, ...state]);
ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id;
ref.read(visibleTabsProvider.notifier).toggleVisibility(id);
unsave();
}

Expand All @@ -97,6 +125,7 @@ class CollectionStateNotifier
.read(requestSequenceProvider.notifier)
.update((state) => [id, ...state]);
ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id;
ref.read(visibleTabsProvider.notifier).toggleVisibility(id);
unsave();
}

Expand All @@ -110,21 +139,24 @@ class CollectionStateNotifier

void remove({String? id}) {
final rId = id ?? ref.read(selectedIdStateProvider);
if (rId == null) return;

var itemIds = ref.read(requestSequenceProvider);
int idx = itemIds.indexOf(rId!);
int idx = itemIds.indexOf(rId);
cancelHttpRequest(rId);
itemIds.remove(rId);
ref.read(requestSequenceProvider.notifier).state = [...itemIds];

ref.read(visibleTabsProvider.notifier).state =
ref.read(visibleTabsProvider).where((tabId) => tabId != rId).toSet();

String? newId;
if (idx == 0 && itemIds.isNotEmpty) {
newId = itemIds[0];
} else if (itemIds.length > 1) {
newId = itemIds[idx - 1];
if (itemIds.isNotEmpty) {
newId = idx < itemIds.length ? itemIds[idx] : itemIds.last;
} else {
newId = null;
}

ref.read(selectedIdStateProvider.notifier).state = newId;

var map = {...state!};
Expand Down Expand Up @@ -175,6 +207,7 @@ class CollectionStateNotifier

ref.read(requestSequenceProvider.notifier).state = [...itemIds];
ref.read(selectedIdStateProvider.notifier).state = newId;
ref.read(visibleTabsProvider.notifier).toggleVisibility(newId);
unsave();
}

Expand All @@ -201,6 +234,7 @@ class CollectionStateNotifier

ref.read(requestSequenceProvider.notifier).state = [...itemIds];
ref.read(selectedIdStateProvider.notifier).state = newId;
ref.read(visibleTabsProvider.notifier).toggleVisibility(newId);
unsave();
}

Expand Down
3 changes: 3 additions & 0 deletions lib/screens/home_page/collection_pane.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ class RequestItem extends ConsumerWidget {
editRequestId: editRequestId,
onTap: () {
ref.read(selectedIdStateProvider.notifier).state = id;
if (!ref.read(visibleTabsProvider).contains(id)) {
ref.read(visibleTabsProvider.notifier).toggleVisibility(id);
}
kHomeScaffoldKey.currentState?.closeDrawer();
},
onSecondaryTap: () {
Expand Down
75 changes: 52 additions & 23 deletions lib/screens/home_page/editor_pane/editor_default.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,62 @@ class RequestEditorDefault extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
return Stack(
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: "Click ",
style: Theme.of(context).textTheme.titleMedium,
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: ElevatedButton(
onPressed: () {
ref.read(collectionStateNotifierProvider.notifier).add();
},
child: const Text(
kLabelPlusNew,
style: kTextStyleButton,
),
),
Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 80.0),
child: Opacity(
opacity: 0.1,
child: const FlutterLogo(
size: 400,
),
// TODO: Replace FlutterLogo with apidash_logo
// child: Image.asset(
// 'assets/apidash_logo.png',
// width: X,
// height: X,
// ),
// OR use SVG :
// child: SvgPicture.asset(
// 'assets/apidash_logo.svg',
// width: X,
// height: X,
// ),
),
),
),
Center(
child: Padding(
padding: const EdgeInsets.only(top: 200.0),
child: Text.rich(
TextSpan(
text: " to start drafting a new API request.",
style: Theme.of(context).textTheme.titleMedium,
children: [
TextSpan(
text: "Click ",
style: Theme.of(context).textTheme.titleMedium,
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: ElevatedButton(
onPressed: () {
ref.read(collectionStateNotifierProvider.notifier).add();
},
child: const Text(
kLabelPlusNew,
style: kTextStyleButton,
),
),
),
TextSpan(
text: " to start drafting a new API request.",
style: Theme.of(context).textTheme.titleMedium,
),
],
),
],
textAlign: TextAlign.center,
),
),
),
],
Expand Down
12 changes: 9 additions & 3 deletions lib/screens/home_page/editor_pane/editor_pane.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'editor_default.dart';
import 'editor_request.dart';
import 'package:apidash/widgets/logo_apidash.dart';

class RequestEditorPane extends ConsumerWidget {
const RequestEditorPane({
Expand All @@ -12,10 +13,15 @@ class RequestEditorPane extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedId = ref.watch(selectedIdStateProvider);
if (selectedId == null) {
final collection = ref.watch(collectionStateNotifierProvider);

if (collection == null || collection.isEmpty) { //NO collection (empty or null) -> Show logo + text
return const RequestEditorDefault();
} else {
return const RequestEditor();
}
if (selectedId == null) { // No selectedId -> Show only the logo
return const LogoApidash();
}

return const RequestEditor();
}
}
17 changes: 10 additions & 7 deletions lib/screens/home_page/editor_pane/editor_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,39 @@ import 'details_card/details_card.dart';
import 'details_card/request_pane/request_pane.dart';
import 'request_editor_top_bar.dart';
import 'url_card.dart';
import 'tab_pane.dart';

class RequestEditor extends StatelessWidget {
const RequestEditor({super.key});

@override
Widget build(BuildContext context) {
return context.isMediumWindow
? const Padding(
? Padding(
padding: kPb10,
child: Column(
children: [
kVSpacer20,
Expanded(
child: EditRequestPane(),
child: const EditRequestPane(),
),
],
),
)
: Padding(
padding: kIsMacOS || kIsWindows ? kPt28o8 : kP8,
child: const Column(
child: Column(
children: [
RequestEditorTopBar(),
EditorPaneRequestURLCard(),
const TabPane(),
kVSpacer10,
Expanded(
const RequestEditorTopBar(),
const EditorPaneRequestURLCard(),
kVSpacer10,
const Expanded(
child: EditorPaneRequestDetailsCard(),
),
],
),
);
}
}
}
69 changes: 69 additions & 0 deletions lib/screens/home_page/editor_pane/tab_pane.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/providers/collection_providers.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/widgets/tab_request_card.dart';
import 'package:apidash/utils/utils.dart';

class TabPane extends ConsumerWidget {
const TabPane({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final collection = ref.watch(collectionStateNotifierProvider);
final requestSequence = ref.watch(requestSequenceProvider);
final selectedId = ref.watch(selectedIdStateProvider);
final visibleTabs = ref.watch(visibleTabsProvider);

// Prevents rendering if data isn’t ready
if (collection == null || requestSequence.isEmpty) {
return const SizedBox.shrink();
}

return Container(
color: Theme.of(context).colorScheme.surfaceContainerLowest,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: SingleChildScrollView( // horizontal scrolling for the tab bar
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
// Filters and maps only visible tabs for rendering
children: requestSequence
.where((id) => visibleTabs.contains(id))
.map((id) {
final request = collection[id]!;
final name = request.name.isNotEmpty
? request.name
: getRequestTitleFromUrl(request.httpRequestModel?.url) ?? 'Untitled';

return TabRequestCard(
apiType: request.apiType,
method: request.httpRequestModel!.method,
name: name,
isSelected: selectedId == id,
onTap: () => ref.read(selectedIdStateProvider.notifier).state = id,
// Manages tab closure and selection adjustment
onClose: () {
ref.read(visibleTabsProvider.notifier).toggleVisibility(id);
if (selectedId == id) {
final remainingTabs = requestSequence.where((tabId) => visibleTabs.contains(tabId) && tabId != id).toList();
if (remainingTabs.isNotEmpty) {
ref.read(selectedIdStateProvider.notifier).state = remainingTabs.first;
} else {
ref.read(selectedIdStateProvider.notifier).state = null;
}
}
},
);
})
.toList(),
),
),
),
);
}
}
29 changes: 29 additions & 0 deletions lib/widgets/logo_apidash.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';

class LogoApidash extends StatelessWidget {
const LogoApidash({super.key});

@override
Widget build(BuildContext context) {
return Center(
child: Opacity(
opacity: 0.1,
child: const FlutterLogo(
size: 400,
),
// TODO: Replace FlutterLogo with apidash_logo
// child: Image.asset(
// 'assets/apidash_logo.png',
// width: X,
// height: X,
// ),
// OR use SVG:
// child: SvgPicture.asset(
// 'assets/apidash_logo.svg',
// width: X,
// height: X,
// ),
),
);
}
}
Loading