Sp 1722 fe implement duplicate space feature (#365)

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1722](https://syncrow.atlassian.net/browse/SP-1722)

## Description

Implemented a feature that allows users to duplicate a space.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1722]:
https://syncrow.atlassian.net/browse/SP-1722?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
This commit is contained in:
Faris Armoush
2025-07-24 10:21:35 +03:00
committed by GitHub
18 changed files with 572 additions and 22 deletions

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart';
@ -51,13 +53,23 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
duration: const Duration(milliseconds: 150),
);
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_centerOnTree();
}
});
}
@override
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedSpace == null) return;
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
if (oldWidget.community.uuid != widget.community.uuid) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_centerOnTree(animate: true);
}
});
} else if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_animateToSpace(widget.selectedSpace);
@ -151,6 +163,60 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_runAnimation(matrix);
}
void _centerOnTree({bool animate = false}) {
if (_positions.isEmpty) {
if (animate) {
_runAnimation(Matrix4.identity());
} else {
_transformationController.value = Matrix4.identity();
}
return;
}
var minX = double.infinity;
var maxX = double.negativeInfinity;
var minY = double.infinity;
var maxY = double.negativeInfinity;
_positions.forEach((uuid, offset) {
final cardWidth = _cardWidths[uuid] ?? _minCardWidth;
minX = min(minX, offset.dx);
maxX = max(maxX, offset.dx + cardWidth);
minY = min(minY, offset.dy);
maxY = max(maxY, offset.dy + _cardHeight);
});
if (!minX.isFinite || !maxX.isFinite || !minY.isFinite || !maxY.isFinite) {
return;
}
final treeWidth = maxX - minX;
final treeHeight = maxY - minY;
final viewSize = context.size;
if (viewSize == null) return;
final scaleX = viewSize.width / treeWidth;
final scaleY = viewSize.height / treeHeight;
final scale = min(scaleX, scaleY).clamp(0.5, 1.0) * 0.9;
final treeCenterX = minX + treeWidth / 2;
final treeCenterY = minY + treeHeight / 2;
final x = -treeCenterX * scale + viewSize.width / 2;
final y = -treeCenterY * scale + viewSize.height / 2;
final matrix = Matrix4.identity()
..translate(x, y)
..scale(scale);
if (animate) {
_runAnimation(matrix);
} else {
_transformationController.value = matrix;
}
}
void _onReorder(SpaceReorderDataModel data, int newIndex) {
final newCommunity = widget.community.copyWith();
final children = data.parent?.children ?? newCommunity.spaces;

View File

@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/views/duplicate_space_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
@ -44,7 +45,22 @@ class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
},
),
),
onDuplicate: (space) {},
onDuplicate: (space) => showDialog<void>(
context: context,
builder: (_) => DuplicateSpaceDialog(
initialName: space.spaceName,
selectedSpaceUuid: space.uuid,
selectedCommunityUuid: selectedCommunity.uuid,
onSuccess: (spaces) {
final updatedCommunity = selectedCommunity.copyWith(
spaces: spaces,
);
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(updatedCommunity),
);
},
),
),
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,

View File

@ -10,21 +10,26 @@ class SpaceManagementBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
return Stack(
children: [
const SpaceManagementCommunitiesTree(),
Expanded(
child: BlocBuilder<CommunitiesTreeSelectionBloc,
CommunitiesTreeSelectionState>(
buildWhen: (previous, current) =>
previous.selectedCommunity != current.selectedCommunity,
builder: (context, state) => Visibility(
visible: state.selectedCommunity == null,
replacement: const SpaceManagementCommunityStructure(),
child: const SpaceManagementTemplatesView(),
Row(
children: [
const SizedBox(width: 320),
Expanded(
child: BlocBuilder<CommunitiesTreeSelectionBloc,
CommunitiesTreeSelectionState>(
buildWhen: (previous, current) =>
previous.selectedCommunity != current.selectedCommunity,
builder: (context, state) => Visibility(
visible: state.selectedCommunity == null,
replacement: const SpaceManagementCommunityStructure(),
child: const SpaceManagementTemplatesView(),
),
),
),
),
],
),
const SpaceManagementCommunitiesTree(),
],
);
}

View File

@ -9,6 +9,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/style.dart';
class SpaceManagementCommunitiesTree extends StatefulWidget {
@ -44,7 +45,15 @@ class _SpaceManagementCommunitiesTreeState
return BlocBuilder<CommunitiesBloc, CommunitiesState>(
builder: (context, state) => Container(
width: 320,
decoration: subSectionContainerDecoration,
decoration: subSectionContainerDecoration.copyWith(
boxShadow: [
BoxShadow(
color: ColorsManager.shadowBlackColor.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(10, 0),
),
],
),
child: Column(
children: [
const SpaceManagementSidebarHeader(),

View File

@ -0,0 +1,60 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/services/duplicate_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
final class RemoteDuplicateSpaceService implements DuplicateSpaceService {
RemoteDuplicateSpaceService(this._httpService);
final HTTPService _httpService;
@override
Future<List<SpaceModel>> duplicateSpace(DuplicateSpaceParam param) async {
try {
final response = await _httpService.post(
path: await _makeUrl(param),
body: param.toJson(),
expectedResponseModel: (json) {
final response = json as Map<String, dynamic>;
final data = response['data'] as List<dynamic>;
return data
.map((e) => SpaceModel.fromJson(e as Map<String, dynamic>))
.toList();
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
throw APIException(errorMessage);
} catch (e) {
throw APIException(e.toString());
}
}
Future<String> _makeUrl(DuplicateSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) {
throw APIException('Project UUID is not set');
}
if (param.communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
if (param.spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
return ApiEndpoints.duplicateSpace
.replaceAll('{projectUuid}', projectUuid)
.replaceAll('{communityUuid}', param.communityUuid)
.replaceAll('{spaceUuid}', param.spaceUuid);
}
}

View File

@ -0,0 +1,15 @@
class DuplicateSpaceParam {
final String communityUuid;
final String spaceUuid;
final String newSpaceName;
DuplicateSpaceParam({
required this.communityUuid,
required this.spaceUuid,
required this.newSpaceName,
});
Map<String, dynamic> toJson() => {
'spaceName': newSpaceName,
};
}

View File

@ -0,0 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.dart';
abstract interface class DuplicateSpaceService {
Future<List<SpaceModel>> duplicateSpace(DuplicateSpaceParam param);
}

View File

@ -0,0 +1,36 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/services/duplicate_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'duplicate_space_event.dart';
part 'duplicate_space_state.dart';
class DuplicateSpaceBloc extends Bloc<DuplicateSpaceEvent, DuplicateSpaceState> {
DuplicateSpaceBloc(
this._duplicateSpaceService,
) : super(const DuplicateSpaceInitial()) {
on<DuplicateSpaceEvent>(_onDuplicateSpaceEvent);
}
final DuplicateSpaceService _duplicateSpaceService;
Future<void> _onDuplicateSpaceEvent(
DuplicateSpaceEvent event,
Emitter<DuplicateSpaceState> emit,
) async {
try {
emit(const DuplicateSpaceLoading());
final result = await _duplicateSpaceService.duplicateSpace(event.param);
emit(DuplicateSpaceSuccess(result));
} on APIException catch (e) {
emit(DuplicateSpaceFailure(e.message));
} catch (e) {
emit(DuplicateSpaceFailure(e.toString()));
} finally {
emit(const DuplicateSpaceInitial());
}
}
}

View File

@ -0,0 +1,10 @@
part of 'duplicate_space_bloc.dart';
final class DuplicateSpaceEvent extends Equatable {
const DuplicateSpaceEvent({required this.param});
final DuplicateSpaceParam param;
@override
List<Object> get props => [param];
}

View File

@ -0,0 +1,34 @@
part of 'duplicate_space_bloc.dart';
sealed class DuplicateSpaceState extends Equatable {
const DuplicateSpaceState();
@override
List<Object> get props => [];
}
final class DuplicateSpaceInitial extends DuplicateSpaceState {
const DuplicateSpaceInitial();
}
final class DuplicateSpaceLoading extends DuplicateSpaceState {
const DuplicateSpaceLoading();
}
final class DuplicateSpaceSuccess extends DuplicateSpaceState {
const DuplicateSpaceSuccess(this.spaces);
final List<SpaceModel> spaces;
@override
List<Object> get props => [spaces];
}
final class DuplicateSpaceFailure extends DuplicateSpaceState {
const DuplicateSpaceFailure(this.errorMessage);
final String errorMessage;
@override
List<Object> get props => [errorMessage];
}

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/app_loading_indicator.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/data/services/remote_duplicate_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_dialog_form.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/extension/app_snack_bar.dart';
class DuplicateSpaceDialog extends StatelessWidget {
const DuplicateSpaceDialog({
required this.initialName,
required this.onSuccess,
required this.selectedSpaceUuid,
required this.selectedCommunityUuid,
super.key,
});
final String initialName;
final void Function(List<SpaceModel> spaces) onSuccess;
final String selectedSpaceUuid;
final String selectedCommunityUuid;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DuplicateSpaceBloc(
RemoteDuplicateSpaceService(HTTPService()),
),
child: BlocListener<DuplicateSpaceBloc, DuplicateSpaceState>(
listener: _listener,
child: DuplicateSpaceDialogForm(
initialName: initialName,
selectedSpaceUuid: selectedSpaceUuid,
selectedCommunityUuid: selectedCommunityUuid,
),
),
);
}
void _listener(BuildContext context, DuplicateSpaceState state) {
switch (state) {
case DuplicateSpaceLoading():
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => const AppLoadingIndicator(),
);
break;
case DuplicateSpaceFailure(:final errorMessage):
Navigator.pop(context);
Navigator.pop(context);
context.showFailureSnackbar(errorMessage);
break;
case DuplicateSpaceSuccess(:final spaces):
onSuccess.call(spaces);
Navigator.of(context).pop();
Navigator.of(context).pop();
context.showSuccessSnackbar('Space duplicated successfully');
break;
default:
break;
}
}
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_text_field.dart';
class DuplicateSpaceDialogForm extends StatefulWidget {
const DuplicateSpaceDialogForm({
required this.initialName,
required this.selectedSpaceUuid,
required this.selectedCommunityUuid,
super.key,
});
final String initialName;
final String selectedSpaceUuid;
final String selectedCommunityUuid;
@override
State<DuplicateSpaceDialogForm> createState() => _DuplicateSpaceDialogFormState();
}
class _DuplicateSpaceDialogFormState extends State<DuplicateSpaceDialogForm> {
late final TextEditingController _nameController;
bool _isNameValid = true;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: '${widget.initialName}(1)');
_nameController.addListener(_validateName);
}
void _validateName() => setState(
() => _isNameValid = _nameController.text.trim() != widget.initialName,
);
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const SelectableText('Duplicate Space'),
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
const SelectableText('Enter a new name for the duplicated space:'),
DuplicateSpaceTextField(
nameController: _nameController,
isNameValid: _isNameValid,
initialName: widget.initialName,
),
],
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('Cancel'),
),
TextButton(
onPressed: _isNameValid ? () => _submit(context) : null,
child: const Text('Duplicate'),
),
],
);
}
void _submit(BuildContext context) {
context.read<DuplicateSpaceBloc>().add(
DuplicateSpaceEvent(
param: DuplicateSpaceParam(
newSpaceName: _nameController.text,
spaceUuid: widget.selectedSpaceUuid,
communityUuid: widget.selectedCommunityUuid,
),
),
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class DuplicateSpaceFailureDialog extends StatelessWidget {
const DuplicateSpaceFailureDialog(this.errorMessage, {super.key});
final String errorMessage;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Failed to duplicate space'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('Close'),
),
],
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DuplicateSpaceTextField extends StatelessWidget {
const DuplicateSpaceTextField({
required this.nameController,
required this.isNameValid,
required this.initialName,
super.key,
});
final TextEditingController nameController;
final bool isNameValid;
final String initialName;
String get _errorText => 'Name must be different from "$initialName"';
@override
Widget build(BuildContext context) {
return TextField(
controller: nameController,
style: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
),
decoration: InputDecoration(
label: const Text('Space Name'),
border: _border(),
enabledBorder: _border(),
focusedBorder: _border(ColorsManager.primaryColor),
errorBorder: _border(context.theme.colorScheme.error),
focusedErrorBorder: _border(context.theme.colorScheme.error),
errorStyle: context.textTheme.bodyMedium!.copyWith(
color: context.theme.colorScheme.error,
fontSize: 8,
),
errorText: isNameValid ? null : _errorText,
),
);
}
OutlineInputBorder _border([Color? color]) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: color ?? ColorsManager.blackColor,
width: 0.5,
),
);
}
}

View File

@ -19,6 +19,7 @@ class SpaceSubSpacesDialog extends StatefulWidget {
}
class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
late final TextEditingController _subspaceNameController;
late List<Subspace> _subspaces;
bool get _hasDuplicateNames =>
@ -29,6 +30,13 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
void initState() {
super.initState();
_subspaces = List.from(widget.subspaces);
_subspaceNameController = TextEditingController();
}
@override
void dispose() {
_subspaceNameController.dispose();
super.dispose();
}
void _handleSubspaceAdded(String name) {
@ -49,6 +57,10 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
);
void _handleSave() {
final name = _subspaceNameController.text.trim();
if (name.isNotEmpty) {
_handleSubspaceAdded(name);
}
widget.onSave(_subspaces);
Navigator.of(context).pop();
}
@ -65,6 +77,7 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
subSpaces: _subspaces,
onSubspaceAdded: _handleSubspaceAdded,
onSubspaceDeleted: _handleSubspaceDeleted,
controller: _subspaceNameController,
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 100),

View File

@ -10,29 +10,28 @@ class SubSpacesInput extends StatefulWidget {
required this.subSpaces,
required this.onSubspaceAdded,
required this.onSubspaceDeleted,
required this.controller,
});
final List<Subspace> subSpaces;
final void Function(String name) onSubspaceAdded;
final void Function(String uuid) onSubspaceDeleted;
final TextEditingController controller;
@override
State<SubSpacesInput> createState() => _SubSpacesInputState();
}
class _SubSpacesInputState extends State<SubSpacesInput> {
late final TextEditingController _subspaceNameController;
late final FocusNode _focusNode;
@override
void initState() {
super.initState();
_subspaceNameController = TextEditingController();
_focusNode = FocusNode();
}
@override
void dispose() {
_subspaceNameController.dispose();
_focusNode.dispose();
super.dispose();
}
@ -81,7 +80,7 @@ class _SubSpacesInputState extends State<SubSpacesInput> {
width: 200,
child: TextField(
focusNode: _focusNode,
controller: _subspaceNameController,
controller: widget.controller,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
@ -93,7 +92,7 @@ class _SubSpacesInputState extends State<SubSpacesInput> {
final trimmedValue = value.trim();
if (trimmedValue.isNotEmpty) {
widget.onSubspaceAdded(trimmedValue);
_subspaceNameController.clear();
widget.controller.clear();
_focusNode.requestFocus();
}
},

View File

@ -141,7 +141,7 @@ abstract class ApiEndpoints {
'/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}';
static const String saveSchedule = '/schedule/{deviceUuid}';
static const String duplicateSpace = '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/duplicate';
////booking System
static const String bookableSpaces = '/bookable-spaces';

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
extension AppSnackBarsBuildContextExtension on BuildContext {
void showSuccessSnackbar(String message) {
ScaffoldMessenger.of(this).showSnackBar(
_makeSnackbar(
message: message,
icon: Icons.check_circle,
backgroundColor: Colors.green,
),
);
}
void showFailureSnackbar(String message) {
ScaffoldMessenger.of(this).showSnackBar(
_makeSnackbar(
message: message,
icon: Icons.error,
backgroundColor: Colors.red,
),
);
}
SnackBar _makeSnackbar({
required String message,
required Color backgroundColor,
required IconData icon,
}) {
return SnackBar(
content: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Icon(icon, color: Colors.white),
Text(
message,
style: textTheme.bodyMedium?.copyWith(
color: ColorsManager.whiteColors,
),
),
],
),
backgroundColor: backgroundColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 92,
vertical: 32,
),
);
}
}