feat: environment pane

This commit is contained in:
DenserMeerkat
2024-06-13 18:47:52 +05:30
parent 09639cdf13
commit 7fcfe4f8e8
22 changed files with 414 additions and 336 deletions

View File

@ -265,7 +265,7 @@ final kColorHttpMethodPut = Colors.amber.shade900;
final kColorHttpMethodPatch = kColorHttpMethodPut; final kColorHttpMethodPatch = kColorHttpMethodPut;
final kColorHttpMethodDelete = Colors.red.shade800; final kColorHttpMethodDelete = Colors.red.shade800;
enum RequestItemMenuOption { edit, delete, duplicate } enum ItemMenuOption { edit, delete, duplicate }
enum HTTPVerb { get, head, post, put, patch, delete } enum HTTPVerb { get, head, post, put, patch, delete }

View File

@ -7,9 +7,13 @@ part 'environment_model.g.dart';
@freezed @freezed
class EnvironmentModel with _$EnvironmentModel { class EnvironmentModel with _$EnvironmentModel {
@JsonSerializable(
explicitToJson: true,
anyMap: true,
)
const factory EnvironmentModel({ const factory EnvironmentModel({
required String id, required String id,
@Default("New Environment") String name, @Default("") String name,
@Default([]) List<EnvironmentVariableModel> values, @Default([]) List<EnvironmentVariableModel> values,
}) = _EnvironmentModel; }) = _EnvironmentModel;
@ -19,6 +23,10 @@ class EnvironmentModel with _$EnvironmentModel {
@freezed @freezed
class EnvironmentVariableModel with _$EnvironmentVariableModel { class EnvironmentVariableModel with _$EnvironmentVariableModel {
@JsonSerializable(
explicitToJson: true,
anyMap: true,
)
const factory EnvironmentVariableModel({ const factory EnvironmentVariableModel({
required String key, required String key,
required String value, required String value,

View File

@ -118,11 +118,12 @@ class __$$EnvironmentModelImplCopyWithImpl<$Res>
} }
/// @nodoc /// @nodoc
@JsonSerializable()
@JsonSerializable(explicitToJson: true, anyMap: true)
class _$EnvironmentModelImpl implements _EnvironmentModel { class _$EnvironmentModelImpl implements _EnvironmentModel {
const _$EnvironmentModelImpl( const _$EnvironmentModelImpl(
{required this.id, {required this.id,
this.name = "New Environment", this.name = "",
final List<EnvironmentVariableModel> values = const []}) final List<EnvironmentVariableModel> values = const []})
: _values = values; : _values = values;
@ -320,7 +321,8 @@ class __$$EnvironmentVariableModelImplCopyWithImpl<$Res>
} }
/// @nodoc /// @nodoc
@JsonSerializable()
@JsonSerializable(explicitToJson: true, anyMap: true)
class _$EnvironmentVariableModelImpl implements _EnvironmentVariableModel { class _$EnvironmentVariableModelImpl implements _EnvironmentVariableModel {
const _$EnvironmentVariableModelImpl( const _$EnvironmentVariableModelImpl(
{required this.key, {required this.key,

View File

@ -6,14 +6,13 @@ part of 'environment_model.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_$EnvironmentModelImpl _$$EnvironmentModelImplFromJson( _$EnvironmentModelImpl _$$EnvironmentModelImplFromJson(Map json) =>
Map<String, dynamic> json) =>
_$EnvironmentModelImpl( _$EnvironmentModelImpl(
id: json['id'] as String, id: json['id'] as String,
name: json['name'] as String? ?? "New Environment", name: json['name'] as String? ?? "",
values: (json['values'] as List<dynamic>?) values: (json['values'] as List<dynamic>?)
?.map((e) => ?.map((e) => EnvironmentVariableModel.fromJson(
EnvironmentVariableModel.fromJson(e as Map<String, dynamic>)) Map<String, Object?>.from(e as Map)))
.toList() ?? .toList() ??
const [], const [],
); );
@ -23,11 +22,11 @@ Map<String, dynamic> _$$EnvironmentModelImplToJson(
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'name': instance.name, 'name': instance.name,
'values': instance.values, 'values': instance.values.map((e) => e.toJson()).toList(),
}; };
_$EnvironmentVariableModelImpl _$$EnvironmentVariableModelImplFromJson( _$EnvironmentVariableModelImpl _$$EnvironmentVariableModelImplFromJson(
Map<String, dynamic> json) => Map json) =>
_$EnvironmentVariableModelImpl( _$EnvironmentVariableModelImpl(
key: json['key'] as String, key: json['key'] as String,
value: json['value'] as String, value: json['value'] as String,

View File

@ -1,12 +1,10 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:apidash/consts.dart';
import '../consts.dart'; import 'providers.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../services/services.dart' show hiveHandler, HiveHandler, request; import '../services/services.dart' show hiveHandler, HiveHandler, request;
import '../utils/utils.dart' show getNewUuid, collectionToHAR; import '../utils/utils.dart' show getNewUuid, collectionToHAR;
import 'settings_providers.dart';
import 'ui_providers.dart';
final selectedIdStateProvider = StateProvider<String?>((ref) => null); final selectedIdStateProvider = StateProvider<String?>((ref) => null);

View File

@ -8,14 +8,13 @@ import '../services/services.dart' show hiveHandler, HiveHandler;
final selectedEnvironmentIdStateProvider = final selectedEnvironmentIdStateProvider =
StateProvider<String?>((ref) => null); StateProvider<String?>((ref) => null);
final environmentsStateNotifierProvider = StateNotifierProvider< final StateNotifierProvider<EnvironmentsStateNotifier,
EnvironmentsStateNotifier, Map<String, EnvironmentModel>?>((ref) { Map<String, EnvironmentModel>?> environmentsStateNotifierProvider =
return EnvironmentsStateNotifier(ref, hiveHandler); StateNotifierProvider((ref) => EnvironmentsStateNotifier(ref, hiveHandler));
});
final environmentSequenceProvider = StateProvider<List<String>>((ref) { final environmentSequenceProvider = StateProvider<List<String>>((ref) {
var ids = hiveHandler.getEnvironmentIds(); var ids = hiveHandler.getEnvironmentIds();
return ids ?? []; return ids ?? [kGlobalEnvironmentId];
}); });
class EnvironmentsStateNotifier class EnvironmentsStateNotifier
@ -25,7 +24,7 @@ class EnvironmentsStateNotifier
Future.microtask(() { Future.microtask(() {
if (status) { if (status) {
ref.read(environmentSequenceProvider.notifier).state = [ ref.read(environmentSequenceProvider.notifier).state = [
state!.keys.first, ...state!.keys,
]; ];
} }
ref.read(selectedEnvironmentIdStateProvider.notifier).state = ref.read(selectedEnvironmentIdStateProvider.notifier).state =
@ -52,16 +51,18 @@ class EnvironmentsStateNotifier
} else { } else {
Map<String, EnvironmentModel> environmentsMap = {}; Map<String, EnvironmentModel> environmentsMap = {};
for (var environmentId in environmentIds) { for (var environmentId in environmentIds) {
var environment = hiveHandler.getEnvironment(environmentId); var jsonModel = hiveHandler.getEnvironment(environmentId);
EnvironmentModel environmentModelFromJson = if (jsonModel != null) {
EnvironmentModel.fromJson(environment); var jsonMap = Map<String, Object?>.from(jsonModel);
var environmentModelFromJson = EnvironmentModel.fromJson(jsonMap);
EnvironmentModel environmentModel = EnvironmentModel( EnvironmentModel environmentModel = EnvironmentModel(
id: environmentModelFromJson.id, id: environmentModelFromJson.id,
name: environmentModelFromJson.name, name: environmentModelFromJson.name,
values: environmentModelFromJson.values, values: environmentModelFromJson.values,
); );
environmentsMap[environmentId] = environmentModel; environmentsMap[environmentId] = environmentModel;
}
} }
state = environmentsMap; state = environmentsMap;
return true; return true;
@ -160,10 +161,10 @@ class EnvironmentsStateNotifier
ref.read(hasUnsavedChangesProvider.notifier).state = true; ref.read(hasUnsavedChangesProvider.notifier).state = true;
} }
void saveEnvironments() async { Future<void> saveEnvironments() async {
ref.read(saveDataStateProvider.notifier).state = true; ref.read(saveDataStateProvider.notifier).state = true;
final environmentIds = ref.read(environmentSequenceProvider); final environmentIds = ref.read(environmentSequenceProvider);
hiveHandler.setEnvironmentIds(environmentIds); await hiveHandler.setEnvironmentIds(environmentIds);
for (var environmentId in environmentIds) { for (var environmentId in environmentIds) {
var environment = state![environmentId]!; var environment = state![environmentId]!;
await hiveHandler.setEnvironment(environmentId, environment.toJson()); await hiveHandler.setEnvironment(environmentId, environment.toJson());

View File

@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
class SidebarHeader extends StatelessWidget {
const SidebarHeader({super.key, this.onAddNew});
final Function()? onAddNew;
@override
Widget build(BuildContext context) {
return Padding(
padding: kPe8,
child: Wrap(
alignment: WrapAlignment.spaceBetween,
children: [
const SaveButton(),
//const Spacer(),
ElevatedButton(
onPressed: onAddNew,
child: const Text(
kLabelPlusNew,
style: kTextStyleButton,
),
),
],
),
);
}
}
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,
),
);
}
}

View File

@ -13,6 +13,7 @@ class Dashboard extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final railIdx = ref.watch(navRailIndexStateProvider); final railIdx = ref.watch(navRailIndexStateProvider);
final mobileScaffoldKey = ref.watch(mobileScaffoldKeyStateProvider);
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
child: Row( child: Row(
@ -93,11 +94,13 @@ class Dashboard extends ConsumerWidget {
child: IndexedStack( child: IndexedStack(
alignment: AlignmentDirectional.topCenter, alignment: AlignmentDirectional.topCenter,
index: railIdx, index: railIdx,
children: const [ children: [
HomePage(), const HomePage(),
EnvironmentPage(), EnvironmentPage(
IntroPage(), scaffoldKey: mobileScaffoldKey,
SettingsPage(), ),
const IntroPage(),
const SettingsPage(),
], ],
), ),
) )

View File

@ -1,12 +1,29 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:apidash/widgets/widgets.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 'environments_pane.dart'; import 'environments_pane.dart';
class EnvironmentPage extends StatelessWidget { class EnvironmentPage extends ConsumerWidget {
const EnvironmentPage({super.key}); const EnvironmentPage({
super.key,
required this.scaffoldKey,
});
final GlobalKey<ScaffoldState> scaffoldKey;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
if (context.isMediumWindow) {
return TwoDrawerScaffold(
scaffoldKey: scaffoldKey,
mainContent: const SizedBox(), // TODO: replace placeholder
title: const Text("Environments"), // TODO: replace placeholder
leftDrawerContent: const EnvironmentsPane(),
onDrawerChanged: (value) =>
ref.read(leftDrawerStateProvider.notifier).state = value,
);
}
return const Column( return const Column(
children: [ children: [
Expanded( Expanded(

View File

@ -1,11 +1,12 @@
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:apidash/widgets/cards.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:apidash/extensions/extensions.dart';
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 'package:apidash/screens/common/sidebar_widgets.dart';
class EnvironmentsPane extends ConsumerWidget { class EnvironmentsPane extends ConsumerWidget {
const EnvironmentsPane({ const EnvironmentsPane({
@ -15,30 +16,31 @@ class EnvironmentsPane extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Padding( return Padding(
padding: kIsMacOS ? kP24CollectionPane : kP8CollectionPane, padding: (!context.isMediumWindow && kIsMacOS
? kP24CollectionPane
: kP8CollectionPane) +
(context.isMediumWindow ? kPb70 : EdgeInsets.zero),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Padding( SidebarHeader(
padding: kPe8, onAddNew: () {
child: Wrap( ref
alignment: WrapAlignment.spaceBetween, .read(environmentsStateNotifierProvider.notifier)
children: [ .addEnvironment();
ElevatedButton( },
onPressed: () {
ref
.read(environmentsStateNotifierProvider.notifier)
.addEnvironment();
},
child: const Text(
kLabelPlusNew,
style: kTextStyleButton,
),
)
],
),
), ),
kVSpacer10,
SidebarFilter(
filterHintText: "Filter by name",
onFilterFieldChanged: (value) {
ref.read(environmentSearchQueryProvider.notifier).state =
value.toLowerCase();
},
),
kVSpacer10,
const Expanded(child: EnvironmentsList()), const Expanded(child: EnvironmentsList()),
kVSpacer5
], ],
), ),
); );
@ -59,81 +61,105 @@ class EnvironmentsList extends HookConsumerWidget {
final filterQuery = ref.watch(environmentSearchQueryProvider).trim(); final filterQuery = ref.watch(environmentSearchQueryProvider).trim();
ScrollController scrollController = useScrollController(); ScrollController scrollController = useScrollController();
return Scrollbar( return Column(
controller: scrollController, children: [
thumbVisibility: alwaysShowEnvironmentsPaneScrollbar, Padding(
radius: const Radius.circular(12), padding: kP1 + kPe8,
child: filterQuery.isEmpty child: EnvironmentItem(
? ReorderableListView.builder( id: kGlobalEnvironmentId,
padding: context.isMediumWindow environmentModel: environmentItems[kGlobalEnvironmentId]!,
? EdgeInsets.only( ),
bottom: MediaQuery.paddingOf(context).bottom, ),
right: 8, const Divider(endIndent: 4),
) Expanded(
: kPe8, child: Scrollbar(
scrollController: scrollController, controller: scrollController,
buildDefaultDragHandles: false, thumbVisibility: alwaysShowEnvironmentsPaneScrollbar,
itemCount: environmentSequence.length, radius: const Radius.circular(12),
onReorder: (int oldIndex, int newIndex) { child: filterQuery.isEmpty
if (oldIndex < newIndex) { ? ReorderableListView.builder(
newIndex -= 1; padding: context.isMediumWindow
} ? EdgeInsets.only(
if (oldIndex != newIndex) { bottom: MediaQuery.paddingOf(context).bottom,
ref right: 8,
.read(collectionStateNotifierProvider.notifier) )
.reorder(oldIndex, newIndex); : kPe8,
} scrollController: scrollController,
}, buildDefaultDragHandles: false,
itemBuilder: (context, index) { itemCount: environmentSequence.length,
var id = environmentSequence[index]; onReorder: (int oldIndex, int newIndex) {
if (kIsMobile) { if (oldIndex < newIndex) {
return ReorderableDelayedDragStartListener( newIndex -= 1;
key: ValueKey(id), }
index: index, if (oldIndex != newIndex) {
child: Padding( ref
padding: kP1, .read(environmentsStateNotifierProvider.notifier)
child: EnvironmentItem( .reorder(oldIndex, newIndex);
id: id, }
environmentModel: environmentItems[id]!, },
), itemBuilder: (context, index) {
), var id = environmentSequence[index];
); if (id == kGlobalEnvironmentId) {
} return SizedBox.shrink(
return ReorderableDragStartListener( key: ValueKey(id),
key: ValueKey(id), );
index: index, }
child: Padding( if (kIsMobile) {
padding: kP1, return ReorderableDelayedDragStartListener(
child: EnvironmentItem( key: ValueKey(id),
id: id, index: index,
environmentModel: environmentItems[id]!, 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 (id == kGlobalEnvironmentId) {
return SizedBox.shrink(
key: ValueKey(id),
);
}
if (item.name.toLowerCase().contains(filterQuery)) {
return Padding(
padding: kP1,
child: EnvironmentItem(
id: id,
environmentModel: item,
),
);
}
return const SizedBox();
}).toList(),
), ),
); ),
}, ),
) ],
: 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(),
),
); );
} }
} }
@ -176,7 +202,29 @@ class EnvironmentItem extends ConsumerWidget {
.updateEnvironment(editRequestId!, name: value); .updateEnvironment(editRequestId!, name: value);
}, },
onTapOutsideNameEditor: () { onTapOutsideNameEditor: () {
ref.read(selectedEnvironmentIdStateProvider.notifier).state = null; ref.read(selectedIdEditStateProvider.notifier).state = null;
},
onMenuSelected: (ItemMenuOption item) {
if (item == ItemMenuOption.edit) {
ref.read(selectedIdEditStateProvider.notifier).state = id;
Future.delayed(
const Duration(milliseconds: 150),
() => ref
.read(nameTextFieldFocusNodeProvider.notifier)
.state
.requestFocus(),
);
}
if (item == ItemMenuOption.delete) {
ref
.read(environmentsStateNotifierProvider.notifier)
.removeEnvironment(id);
}
if (item == ItemMenuOption.duplicate) {
ref
.read(environmentsStateNotifierProvider.notifier)
.duplicateEnvironment(id);
}
}, },
); );
} }

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/providers/providers.dart'; import 'package:apidash/providers/providers.dart';
import 'package:apidash/extensions/extensions.dart';
import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/models/models.dart'; import 'package:apidash/models/models.dart';
import 'package:apidash/consts.dart'; import 'package:apidash/consts.dart';
import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/screens/common/sidebar_widgets.dart';
class CollectionPane extends ConsumerWidget { class CollectionPane extends ConsumerWidget {
const CollectionPane({ const CollectionPane({
@ -13,110 +14,39 @@ class CollectionPane extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final overlayWidget = OverlayWidgetTemplate(context: context);
final collection = ref.watch(collectionStateNotifierProvider); final collection = ref.watch(collectionStateNotifierProvider);
final savingData = ref.watch(saveDataStateProvider);
final hasUnsavedChanges = ref.watch(hasUnsavedChangesProvider);
if (collection == null) { if (collection == null) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
return Drawer( return Padding(
shape: const ContinuousRectangleBorder(), padding: (!context.isMediumWindow && kIsMacOS
backgroundColor: Theme.of(context).colorScheme.surface, ? kP24CollectionPane
surfaceTintColor: kColorTransparent, : kP8CollectionPane) +
child: Padding( (context.isMediumWindow ? kPb70 : EdgeInsets.zero),
padding: (!context.isMediumWindow && kIsMacOS child: Column(
? kP24CollectionPane crossAxisAlignment: CrossAxisAlignment.stretch,
: kP8CollectionPane) + children: [
(context.isMediumWindow ? kPb70 : EdgeInsets.zero), SidebarHeader(
child: Column( onAddNew: () {
crossAxisAlignment: CrossAxisAlignment.stretch, ref.read(collectionStateNotifierProvider.notifier).add();
children: [ },
Padding( ),
padding: kPe8, kVSpacer10,
child: Wrap( SidebarFilter(
alignment: WrapAlignment.spaceBetween, filterHintText: "Filter by name or url",
children: [ onFilterFieldChanged: (value) {
TextButton.icon( ref.read(collectionSearchQueryProvider.notifier).state =
onPressed: (savingData || !hasUnsavedChanges) value.toLowerCase();
? null },
: () async { ),
overlayWidget.show( kVSpacer10,
widget: const Expanded(
const SavingOverlay(saveCompleted: false)); child: RequestList(),
),
await ref kVSpacer5
.read(collectionStateNotifierProvider.notifier) ],
.saveData();
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,
),
),
//const Spacer(),
ElevatedButton(
onPressed: () {
ref.read(collectionStateNotifierProvider.notifier).add();
},
child: const Text(
kLabelPlusNew,
style: kTextStyleButton,
),
),
],
),
),
kVSpacer10,
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: "Filter by name or URL",
onChanged: (value) {
ref.read(collectionSearchQueryProvider.notifier).state =
value.toLowerCase();
},
),
),
],
),
),
kVSpacer10,
const Expanded(
child: RequestList(),
),
kVSpacer5
],
),
), ),
); );
} }
@ -277,8 +207,8 @@ class RequestItem extends ConsumerWidget {
onTapOutsideNameEditor: () { onTapOutsideNameEditor: () {
ref.read(selectedIdEditStateProvider.notifier).state = null; ref.read(selectedIdEditStateProvider.notifier).state = null;
}, },
onMenuSelected: (RequestItemMenuOption item) { onMenuSelected: (ItemMenuOption item) {
if (item == RequestItemMenuOption.edit) { if (item == ItemMenuOption.edit) {
// var controller = // var controller =
// ref.read(nameTextFieldControllerProvider.notifier).state; // ref.read(nameTextFieldControllerProvider.notifier).state;
// controller.text = requestModel.name; // controller.text = requestModel.name;
@ -294,10 +224,10 @@ class RequestItem extends ConsumerWidget {
.requestFocus(), .requestFocus(),
); );
} }
if (item == RequestItemMenuOption.delete) { if (item == ItemMenuOption.delete) {
ref.read(collectionStateNotifierProvider.notifier).remove(id); ref.read(collectionStateNotifierProvider.notifier).remove(id);
} }
if (item == RequestItemMenuOption.duplicate) { if (item == ItemMenuOption.duplicate) {
ref.read(collectionStateNotifierProvider.notifier).duplicate(id); ref.read(collectionStateNotifierProvider.notifier).duplicate(id);
} }
}, },

View File

@ -1,4 +1,4 @@
import 'package:apidash/consts.dart'; import 'package:apidash/screens/envvar/environment_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -69,19 +69,8 @@ class PageBranch extends ConsumerWidget {
switch (pageIndex) { switch (pageIndex) {
case 1: case 1:
// Temporary Environment Page // Temporary Environment Page
return Scaffold( return EnvironmentPage(
key: scaffoldKey, scaffoldKey: scaffoldKey,
appBar: AppBar(
title: const Text('Environments'),
),
onDrawerChanged: (isOpened) {
ref.read(leftDrawerStateProvider.notifier).state = isOpened;
},
drawer: const Drawer(
surfaceTintColor: kColorTransparent,
shape: ContinuousRectangleBorder(),
),
body: const SizedBox(),
); );
case 2: case 2:
return const PageBase( return const PageBase(

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/extensions/extensions.dart';
import 'package:apidash/providers/providers.dart'; import 'package:apidash/providers/providers.dart';
class BottomNavBar extends ConsumerWidget { class BottomNavBar extends ConsumerWidget {

View File

@ -47,7 +47,7 @@ class RequestTitle extends ConsumerWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: RequestCardMenu( child: ItemCardMenu(
offset: const Offset(0, 40), offset: const Offset(0, 40),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
splashRadius: 0, splashRadius: 0,
@ -75,18 +75,18 @@ class RequestTitle extends ConsumerWidget {
], ],
), ),
), ),
onSelected: (RequestItemMenuOption item) { onSelected: (ItemMenuOption item) {
if (item == RequestItemMenuOption.edit) { if (item == ItemMenuOption.edit) {
showRenameDialog(context, name, (val) { showRenameDialog(context, name, (val) {
ref ref
.read(collectionStateNotifierProvider.notifier) .read(collectionStateNotifierProvider.notifier)
.update(id!, name: val); .update(id!, name: val);
}); });
} }
if (item == RequestItemMenuOption.delete) { if (item == ItemMenuOption.delete) {
ref.read(collectionStateNotifierProvider.notifier).remove(id!); ref.read(collectionStateNotifierProvider.notifier).remove(id!);
} }
if (item == RequestItemMenuOption.duplicate) { if (item == ItemMenuOption.duplicate) {
ref.read(collectionStateNotifierProvider.notifier).duplicate(id!); ref.read(collectionStateNotifierProvider.notifier).duplicate(id!);
} }
}, },

View File

@ -0,0 +1,6 @@
String getEnvironmentTitle(String? name) {
if (name == null || name.trim() == "") {
return "untitled";
}
return name;
}

View File

@ -4,3 +4,4 @@ export 'http_utils.dart';
export 'file_utils.dart'; export 'file_utils.dart';
export 'window_utils.dart'; export 'window_utils.dart';
export 'har_utils.dart'; export 'har_utils.dart';
export 'envvar_utils.dart';

View File

@ -212,30 +212,6 @@ class DiscordButton extends StatelessWidget {
} }
} }
class SaveButton extends StatelessWidget {
const SaveButton({
super.key,
this.onPressed,
});
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return TextButton.icon(
onPressed: onPressed,
icon: const Icon(
Icons.save,
size: 20,
),
label: const Text(
kLabelSave,
style: kTextStyleButton,
),
);
}
}
class ClearResponseButton extends StatelessWidget { class ClearResponseButton extends StatelessWidget {
const ClearResponseButton({ const ClearResponseButton({
super.key, super.key,

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:apidash/consts.dart'; import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart'; import 'package:apidash/utils/utils.dart';
import 'menus.dart' show RequestCardMenu; import 'menus.dart' show ItemCardMenu;
import 'texts.dart' show MethodBox; import 'texts.dart' show MethodBox;
class SidebarRequestCard extends StatelessWidget { class SidebarRequestCard extends StatelessWidget {
@ -36,7 +36,7 @@ class SidebarRequestCard extends StatelessWidget {
// final TextEditingController? controller; // final TextEditingController? controller;
final FocusNode? focusNode; final FocusNode? focusNode;
final Function()? onTapOutsideNameEditor; final Function()? onTapOutsideNameEditor;
final Function(RequestItemMenuOption)? onMenuSelected; final Function(ItemMenuOption)? onMenuSelected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -118,7 +118,7 @@ class SidebarRequestCard extends StatelessWidget {
visible: isSelected && !inEditMode, visible: isSelected && !inEditMode,
child: SizedBox( child: SizedBox(
width: 28, width: 28,
child: RequestCardMenu( child: ItemCardMenu(
onSelected: onMenuSelected, onSelected: onMenuSelected,
), ),
), ),
@ -185,19 +185,19 @@ class SidebarEnvironmentCard extends StatelessWidget {
final Function(String)? onChangedNameEditor; final Function(String)? onChangedNameEditor;
final FocusNode? focusNode; final FocusNode? focusNode;
final Function()? onTapOutsideNameEditor; final Function()? onTapOutsideNameEditor;
final Function(RequestItemMenuOption)? onMenuSelected; final Function(ItemMenuOption)? onMenuSelected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color color = Theme.of(context).colorScheme.surface; final colorScheme = Theme.of(context).colorScheme;
final Color colorVariant = final Color color = colorScheme.surface;
Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5); final Color colorVariant = colorScheme.surfaceVariant.withOpacity(0.5);
final Color surfaceTint = Theme.of(context).colorScheme.primary; final Color surfaceTint = colorScheme.primary;
bool isSelected = selectedId == id; bool isSelected = selectedId == id;
bool inEditMode = editRequestId == id; bool inEditMode = editRequestId == id;
final colorScheme = Theme.of(context).colorScheme; String nm = getEnvironmentTitle(name);
return Tooltip( return Tooltip(
message: name, message: nm,
triggerMode: TooltipTriggerMode.manual, triggerMode: TooltipTriggerMode.manual,
waitDuration: const Duration(seconds: 1), waitDuration: const Duration(seconds: 1),
child: Card( child: Card(
@ -230,22 +230,6 @@ class SidebarEnvironmentCard extends StatelessWidget {
height: 20, height: 20,
child: Row( child: Row(
children: [ children: [
isGlobal
? const SizedBox.shrink()
: Checkbox(
value: isActive,
onChanged: isActive ? null : setActive,
shape: const CircleBorder(),
checkColor: colorScheme.onPrimary,
fillColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return colorScheme.primary;
}
return null;
},
),
),
kHSpacer4, kHSpacer4,
Expanded( Expanded(
child: inEditMode child: inEditMode
@ -271,16 +255,16 @@ class SidebarEnvironmentCard extends StatelessWidget {
), ),
) )
: Text( : Text(
name ?? "h", nm,
softWrap: false, softWrap: false,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
), ),
), ),
Visibility( Visibility(
visible: isSelected && !inEditMode, visible: isSelected && !inEditMode && !isGlobal,
child: SizedBox( child: SizedBox(
width: 28, width: 28,
child: RequestCardMenu( child: ItemCardMenu(
onSelected: onMenuSelected, onSelected: onMenuSelected,
), ),
), ),

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:apidash/consts.dart'; import 'package:apidash/consts.dart';
class RequestCardMenu extends StatelessWidget { class ItemCardMenu extends StatelessWidget {
const RequestCardMenu({ const ItemCardMenu({
super.key, super.key,
this.onSelected, this.onSelected,
this.child, this.child,
@ -17,11 +17,11 @@ class RequestCardMenu extends StatelessWidget {
final String? tooltip; final String? tooltip;
final ShapeBorder? shape; final ShapeBorder? shape;
final Function(RequestItemMenuOption)? onSelected; final Function(ItemMenuOption)? onSelected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopupMenuButton<RequestItemMenuOption>( return PopupMenuButton<ItemMenuOption>(
tooltip: tooltip, tooltip: tooltip,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
splashRadius: splashRadius, splashRadius: splashRadius,
@ -29,18 +29,17 @@ class RequestCardMenu extends StatelessWidget {
offset: offset, offset: offset,
onSelected: onSelected, onSelected: onSelected,
shape: shape, shape: shape,
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) => <PopupMenuEntry<ItemMenuOption>>[
<PopupMenuEntry<RequestItemMenuOption>>[ const PopupMenuItem<ItemMenuOption>(
const PopupMenuItem<RequestItemMenuOption>( value: ItemMenuOption.edit,
value: RequestItemMenuOption.edit,
child: Text('Rename'), child: Text('Rename'),
), ),
const PopupMenuItem<RequestItemMenuOption>( const PopupMenuItem<ItemMenuOption>(
value: RequestItemMenuOption.delete, value: ItemMenuOption.delete,
child: Text('Delete'), child: Text('Delete'),
), ),
const PopupMenuItem<RequestItemMenuOption>( const PopupMenuItem<ItemMenuOption>(
value: RequestItemMenuOption.duplicate, value: ItemMenuOption.duplicate,
child: Text('Duplicate'), child: Text('Duplicate'),
), ),
], ],

View File

@ -179,7 +179,12 @@ class TwoDrawerScaffold extends StatelessWidget {
: const SizedBox.shrink()), : const SizedBox.shrink()),
], ],
), ),
drawer: leftDrawerContent, drawer: Drawer(
shape: const ContinuousRectangleBorder(),
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: kColorTransparent,
child: leftDrawerContent,
),
endDrawer: rightDrawerContent, endDrawer: rightDrawerContent,
body: mainContent, body: mainContent,
bottomNavigationBar: bottomNavigationBar, bottomNavigationBar: bottomNavigationBar,

View File

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:apidash/widgets/buttons.dart'; import 'package:apidash/widgets/buttons.dart';
import 'package:apidash/screens/common/sidebar_widgets.dart';
import 'package:apidash/consts.dart'; import 'package:apidash/consts.dart';
import '../test_consts.dart'; import '../test_consts.dart';

View File

@ -5,7 +5,7 @@ import 'package:apidash/consts.dart';
import '../test_consts.dart'; import '../test_consts.dart';
void main() { void main() {
testWidgets('Testing RequestCardMenu', (tester) async { testWidgets('Testing ItemCardMenu', (tester) async {
dynamic changedValue; dynamic changedValue;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
@ -15,7 +15,7 @@ void main() {
body: Center( body: Center(
child: Column( child: Column(
children: [ children: [
RequestCardMenu( ItemCardMenu(
onSelected: (value) { onSelected: (value) {
changedValue = value; changedValue = value;
}, },
@ -27,9 +27,9 @@ void main() {
), ),
); );
expect(find.byType(PopupMenuButton<RequestItemMenuOption>), findsOneWidget); expect(find.byType(PopupMenuButton<ItemMenuOption>), findsOneWidget);
await tester.tap(find.byType(PopupMenuButton<RequestItemMenuOption>)); await tester.tap(find.byType(PopupMenuButton<ItemMenuOption>));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
@ -37,9 +37,9 @@ void main() {
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(changedValue, RequestItemMenuOption.delete); expect(changedValue, ItemMenuOption.delete);
await tester.tap(find.byType(PopupMenuButton<RequestItemMenuOption>)); await tester.tap(find.byType(PopupMenuButton<ItemMenuOption>));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
@ -47,6 +47,6 @@ void main() {
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(changedValue, RequestItemMenuOption.duplicate); expect(changedValue, ItemMenuOption.duplicate);
}); });
} }