From ff3d5cd996d4493d2bf89dbc125c45b074d6528f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:02:02 +0300 Subject: [PATCH 1/9] Created a helper class to show create community dialog, since this dialog can be shown from two different widgets. --- ...ce_management_community_dialog_helper.dart | 24 +++++++++++++++++++ .../space_management_sidebar_header.dart | 20 ++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart new file mode 100644 index 00000000..a18834df --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +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/create_community/presentation/create_community_dialog.dart'; + +abstract final class SpaceManagementCommunityDialogHelper { + static void showCreateDialog(BuildContext context) { + showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart index 25c094db..b5f2a1b7 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.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/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -41,7 +40,7 @@ class SpaceManagementSidebarHeader extends StatelessWidget { if (isSelected) { _clearSelection(context); } else { - _showCreateCommunityDialog(context); + SpaceManagementCommunityDialogHelper.showCreateDialog(context); } } @@ -50,19 +49,4 @@ class SpaceManagementSidebarHeader extends StatelessWidget { const ClearCommunitiesTreeSelectionEvent(), ); } - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), - ); } From 0e7109a19e297bb461a8214d01c504d5b0ab6098 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:02:15 +0300 Subject: [PATCH 2/9] Created `CommunityTemplateCell` widget. --- .../widgets/community_template_cell.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart new file mode 100644 index 00000000..4352d069 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CommunityTemplateCell extends StatelessWidget { + const CommunityTemplateCell({ + super.key, + required this.onTap, + required this.title, + }); + + final void Function() onTap; + final Widget title; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: AspectRatio( + aspectRatio: 2.0, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 4, + strokeAlign: BorderSide.strokeAlignOutside, + color: ColorsManager.borderColor, + ), + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ), + ), + DefaultTextStyle( + style: context.textTheme.bodyLarge!.copyWith( + color: ColorsManager.blackColor, + ), + child: title, + ), + ], + ), + ); + } +} From a78b5993a9aa6c7e425474c0c23f30d3b3c2cadd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:05:53 +0300 Subject: [PATCH 3/9] Created `SpaceManagementTemplatesView` widget. --- .../space_management_templates_view.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart new file mode 100644 index 00000000..dd46d2c1 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceManagementTemplatesView extends StatelessWidget { + const SpaceManagementTemplatesView({super.key}); + @override + Widget build(BuildContext context) { + return Expanded( + child: ColoredBox( + color: ColorsManager.whiteColors, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.0, + ), + itemCount: _gridItems(context).length, + itemBuilder: (context, index) { + final model = _gridItems(context)[index]; + return CommunityTemplateCell( + onTap: model.onTap, + title: model.title, + ); + }, + ), + ), + ); + } + + List<_CommunityTemplateModel> _gridItems(BuildContext context) { + return [ + _CommunityTemplateModel( + title: const Text('Blank'), + onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context), + ), + ]; + } +} + +class _CommunityTemplateModel { + final Widget title; + final void Function() onTap; + + _CommunityTemplateModel({ + required this.title, + required this.onTap, + }); +} From 7d4cdba0eff5c9e09ac075f98302ea9a8857c173 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:06:59 +0300 Subject: [PATCH 4/9] Connected templates view into `SpaceManagementBody`, while applying the correct UI principals if what to show what when? --- .../widgets/space_management_body.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 3a9aa3c8..5d28a533 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.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/communities/presentation/widgets/space_management_communities_tree.dart'; class SpaceManagementBody extends StatelessWidget { @@ -6,9 +9,21 @@ class SpaceManagementBody extends StatelessWidget { @override Widget build(BuildContext context) { - return const Row( + return Row( children: [ - SpaceManagementCommunitiesTree(), + const SpaceManagementCommunitiesTree(), + Expanded( + child: BlocBuilder( + buildWhen: (previous, current) => + previous.selectedCommunity != current.selectedCommunity, + builder: (context, state) => Visibility( + visible: state.selectedCommunity == null, + replacement: const Placeholder(), + child: const SpaceManagementTemplatesView(), + ), + ), + ), ], ); } From f8e4c89cdb63e50ef6cc1323a298f603690f36cf Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:11:03 +0300 Subject: [PATCH 5/9] uses correct error message that the api sends in `RemoteCreateCommunityService`. --- .../data/services/remote_create_community_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index bd91f6ce..aae92e9f 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -53,7 +53,7 @@ class RemoteCreateCommunityService implements CreateCommunityService { return _defaultErrorMessage; } final error = body['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; + final errorMessage = error?['message'] as String? ?? ''; return errorMessage; } From 4bdb487094fc81d9786acaacf2fcceccc8ee6f2b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:11:23 +0300 Subject: [PATCH 6/9] doesnt show a snackbar when creating a community fails, since we show the error message in the dialog itself. --- .../presentation/create_community_dialog.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart index 8c1d474d..a9af44d6 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -41,11 +41,8 @@ class CreateCommunityDialog extends StatelessWidget { ); onCreateCommunity.call(community); break; - case CreateCommunityFailure(:final message): + case CreateCommunityFailure(): Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); break; default: break; From ada7daf17950efb2aba67d39efd867d796f2facd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:13:30 +0300 Subject: [PATCH 7/9] Switched from using `Text` to `SelectableText` in `CreateCommunityDialog`. --- .../helpers/space_management_community_dialog_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart index a18834df..5322c3ea 100644 --- a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -9,7 +9,7 @@ abstract final class SpaceManagementCommunityDialogHelper { showDialog( context: context, builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), + title: const SelectableText('Community Name'), onCreateCommunity: (community) { context.read().add( InsertCommunity(community), From 8bc7a3daa2b6cc3fdd44767186f6536ad6b8a0af Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 15:45:49 +0300 Subject: [PATCH 8/9] Implemented space management canvas. --- .../models/space_connection_model.dart | 6 + .../spaces_connections_arrow_painter.dart | 60 +++++ .../widgets/community_structure_canvas.dart | 236 ++++++++++++++++++ .../widgets/create_space_button.dart | 42 ++++ .../widgets/plus_button_widget.dart | 43 ++++ .../widgets/space_card_widget.dart | 65 +++++ .../main_module/widgets/space_cell.dart | 88 +++++++ .../widgets/space_management_body.dart | 3 +- .../space_management_community_structure.dart | 22 ++ .../communities_tree_selection_bloc.dart | 2 +- .../communities_tree_selection_event.dart | 7 +- ...anagement_communities_tree_space_tile.dart | 2 +- lib/utils/app_routes.dart | 2 +- 13 files changed, 571 insertions(+), 7 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/models/space_connection_model.dart create mode 100644 lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/create_space_button.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_cell.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart diff --git a/lib/pages/space_management_v2/main_module/models/space_connection_model.dart b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart new file mode 100644 index 00000000..538a922c --- /dev/null +++ b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart @@ -0,0 +1,6 @@ +class SpaceConnectionModel { + final String from; + final String to; + + const SpaceConnectionModel({required this.from, required this.to}); +} diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart new file mode 100644 index 00000000..fcf523bf --- /dev/null +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpacesConnectionsArrowPainter extends CustomPainter { + final List connections; + final Map positions; + final double cardWidth = 150.0; + final double cardHeight = 90.0; + final String? selectedSpaceUuid; + + SpacesConnectionsArrowPainter({ + required this.connections, + required this.positions, + this.selectedSpaceUuid, + }); + + @override + void paint(Canvas canvas, Size size) { + for (final connection in connections) { + final isSelected = connection.to == selectedSpaceUuid; + final paint = Paint() + ..color = isSelected + ? ColorsManager.primaryColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + final from = positions[connection.from]; + final to = positions[connection.to]; + + if (from != null && to != null) { + final startPoint = + Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10); + final endPoint = Offset(to.dx + cardWidth / 2, to.dy); + + final path = Path()..moveTo(startPoint.dx, startPoint.dy); + + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60); + final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); + + path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, + controlPoint2.dy, endPoint.dx, endPoint.dy); + + canvas.drawPath(path, paint); + + final circlePaint = Paint() + ..color = isSelected + ? ColorsManager.primaryColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.srcIn; + canvas.drawCircle(endPoint, 4, circlePaint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart new file mode 100644 index 00000000..92c5add6 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +class CommunityStructureCanvas extends StatefulWidget { + const CommunityStructureCanvas({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + State createState() =>_CommunityStructureCanvasState(); +} + +class _CommunityStructureCanvasState extends State + with SingleTickerProviderStateMixin { + final Map _positions = {}; + final double _cardWidth = 150.0; + final double _cardHeight = 90.0; + final double _horizontalSpacing = 150.0; + final double _verticalSpacing = 120.0; + String? _selectedSpaceUuid; + + late TransformationController _transformationController; + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _transformationController = TransformationController(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + } + + @override + void dispose() { + _transformationController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + void _runAnimation(Matrix4 target) { + final animation = Matrix4Tween( + begin: _transformationController.value, + end: target, + ).animate(_animationController); + + void listener() { + _transformationController.value = animation.value; + } + + animation.addListener(listener); + _animationController.forward(from: 0).whenCompleteOrCancel(() { + animation.removeListener(listener); + }); + } + + void _onSpaceTapped(String spaceUuid) { + setState(() { + _selectedSpaceUuid = spaceUuid; + }); + + final position = _positions[spaceUuid]; + if (position == null) return; + + const scale = 2.0; + final viewSize = context.size; + if (viewSize == null) return; + + final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2); + final y = + -position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2); + + final matrix = Matrix4.identity() + ..translate(x, y) + ..scale(scale); + + _runAnimation(matrix); + } + + void _resetSelectionAndZoom() { + setState(() { + _selectedSpaceUuid = null; + }); + _runAnimation(Matrix4.identity()); + } + + void _calculateLayout( + List spaces, + int depth, + Map levelXOffset, + ) { + for (final space in spaces) { + double childSubtreeWidth = 0; + if (space.children.isNotEmpty) { + _calculateLayout(space.children, depth + 1, levelXOffset); + final firstChildPos = _positions[space.children.first.uuid]; + final lastChildPos = _positions[space.children.last.uuid]; + if (firstChildPos != null && lastChildPos != null) { + childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx; + } + } + + final currentX = levelXOffset.putIfAbsent(depth, () => 0.0); + double? x; + + if (space.children.isNotEmpty) { + final firstChildPos = _positions[space.children.first.uuid]!; + x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2; + } else { + x = currentX; + } + + if (x < currentX) { + final shiftX = currentX - x; + _shiftSubtree(space, shiftX); + final keysToShift = levelXOffset.keys.where((d) => d > depth).toList(); + for (final key in keysToShift) { + levelXOffset[key] = levelXOffset[key]! + shiftX; + } + x += shiftX; + } + + final y = depth * (_verticalSpacing + _cardHeight); + _positions[space.uuid] = Offset(x, y); + levelXOffset[depth] = x + _cardWidth + _horizontalSpacing; + } + } + + void _shiftSubtree(SpaceModel space, double shiftX) { + if (_positions.containsKey(space.uuid)) { + _positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0); + } + for (final child in space.children) { + _shiftSubtree(child, shiftX); + } + } + + List _buildTreeWidgets() { + _positions.clear(); + final community = widget.community; + + _calculateLayout(community.spaces, 0, {}); + + final widgets = []; + final connections = []; + _generateWidgets(community.spaces, widgets, connections); + + return [ + CustomPaint( + painter: SpacesConnectionsArrowPainter( + connections: connections, + positions: _positions, + selectedSpaceUuid: _selectedSpaceUuid, + ), + child: Stack(alignment: AlignmentDirectional.center, children: widgets), + ), + ]; + } + + void _generateWidgets( + List spaces, + List widgets, + List connections, + ) { + for (final space in spaces) { + final position = _positions[space.uuid]; + if (position == null) continue; + + widgets.add( + Positioned( + left: position.dx, + top: position.dy, + width: _cardWidth, + height: _cardHeight, + child: SpaceCardWidget( + index: spaces.indexOf(space), + onPositionChanged: (newPosition) {}, + buildSpaceContainer: (index) { + return Opacity( + opacity: 1.0, + child: SpaceCell( + index: index, + onTap: () => _onSpaceTapped(space.uuid), + icon: space.icon, + name: space.spaceName, + ), + ); + }, + screenSize: MediaQuery.sizeOf(context), + position: position, + isHovered: false, + onHoverChanged: (int index, bool isHovered) {}, + onButtonTap: (int index, Offset newPosition) {}, + ), + ), + ); + + for (final child in space.children) { + connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + } + _generateWidgets(space.children, widgets, connections); + } + } + + @override + Widget build(BuildContext context) { + final treeWidgets = _buildTreeWidgets(); + return InteractiveViewer( + transformationController: _transformationController, + boundaryMargin: EdgeInsets.symmetric( + horizontal: MediaQuery.sizeOf(context).width * 0.3, + vertical: MediaQuery.sizeOf(context).height * 0.2, + ), + minScale: 0.5, + maxScale: 3.0, + constrained: false, + child: GestureDetector( + onTap: _resetSelectionAndZoom, + child: SizedBox( + width: MediaQuery.sizeOf(context).width * 2, + height: MediaQuery.sizeOf(context).height * 2, + child: Stack(children: treeWidgets), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart new file mode 100644 index 00000000..5caf6a81 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSpaceButton extends StatelessWidget { + const CreateSpaceButton({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () {}, + child: Container( + height: 60, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: Colors.blue, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart new file mode 100644 index 00000000..755a6ab9 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PlusButtonWidget extends StatelessWidget { + final int index; + final String direction; + final Offset offset; + final void Function(int index, Offset newPosition) onButtonTap; + + const PlusButtonWidget({ + super.key, + required this.index, + required this.direction, + required this.offset, + required this.onButtonTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (direction == 'down') { + onButtonTap(index, const Offset(0, 150)); + } else { + onButtonTap(index, const Offset(150, 0)); + } + }, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: ColorsManager.whiteColors, + size: 20, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart new file mode 100644 index 00000000..1ce28502 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart'; + +class SpaceCardWidget extends StatelessWidget { + final int index; + final Size screenSize; + final Offset position; + final bool isHovered; + final void Function(int index, bool isHovered) onHoverChanged; + final void Function(int index, Offset newPosition) onButtonTap; + final Widget Function(int index) buildSpaceContainer; + final ValueChanged onPositionChanged; + + const SpaceCardWidget({ + super.key, + required this.index, + required this.onPositionChanged, + required this.screenSize, + required this.position, + required this.isHovered, + required this.onHoverChanged, + required this.onButtonTap, + required this.buildSpaceContainer, + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => onHoverChanged(index, true), + onExit: (_) => onHoverChanged(index, false), + child: SizedBox( + width: 150, + height: 90, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + buildSpaceContainer(index), + + if (isHovered) + Positioned( + bottom: 0, + child: PlusButtonWidget( + index: index, + direction: 'down', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), + ), + if (isHovered) + Positioned( + right: -15, + child: PlusButtonWidget( + index: index, + direction: 'right', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart new file mode 100644 index 00000000..1b08835a --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceCell extends StatelessWidget { + final int index; + final String icon; + final String name; + final VoidCallback? onDoubleTap; + final VoidCallback? onTap; + + const SpaceCell({ + super.key, + required this.index, + required this.icon, + required this.name, + this.onTap, + this.onDoubleTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GestureDetector( + onDoubleTap: onDoubleTap, + onTap: onTap, + child: Container( + width: 150, + height: 70, + decoration: _containerDecoration(), + child: Row( + children: [ + _buildIconContainer(), + const SizedBox(width: 10), + Expanded( + child: Text( + name, + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } + + Widget _buildIconContainer() { + return Container( + width: 40, + height: double.infinity, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + child: Center( + child: SvgPicture.asset( + icon, + color: ColorsManager.whiteColors, + width: 24, + height: 24, + ), + ), + ); + } + + BoxDecoration _containerDecoration() { + return BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: ColorsManager.lightGrayColor.withValues(alpha: 0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 5d28a533..5d81bffb 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.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/communities/presentation/widgets/space_management_communities_tree.dart'; @@ -19,7 +20,7 @@ class SpaceManagementBody extends StatelessWidget { previous.selectedCommunity != current.selectedCommunity, builder: (context, state) => Visibility( visible: state.selectedCommunity == null, - replacement: const Placeholder(), + replacement: const SpaceManagementCommunityStructure(), child: const SpaceManagementTemplatesView(), ), ), diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart new file mode 100644 index 00000000..11ee5078 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; + +class SpaceManagementCommunityStructure extends StatelessWidget { + const SpaceManagementCommunityStructure({super.key}); + + @override + Widget build(BuildContext context) { + final selectedCommunity = + context.watch().state.selectedCommunity!; + const spacer = Spacer(flex: 10); + return Visibility( + visible: selectedCommunity.spaces.isNotEmpty, + replacement: const Row( + children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), + child: CommunityStructureCanvas(community: selectedCommunity), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart index bfc02f11..bdda04ee 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -32,7 +32,7 @@ class CommunitiesTreeSelectionBloc ) { emit( CommunitiesTreeSelectionState( - selectedCommunity: null, + selectedCommunity: event.community, selectedSpace: event.space, ), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 95ffe173..40a41f74 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -8,7 +8,7 @@ sealed class CommunitiesTreeSelectionEvent extends Equatable { } final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { - final CommunityModel? community; + final CommunityModel community; const SelectCommunityEvent({required this.community}); @override @@ -16,9 +16,10 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { } final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { - final SpaceModel? space; + final SpaceModel space; + final CommunityModel community; - const SelectSpaceEvent({required this.space}); + const SelectSpaceEvent({required this.space, required this.community}); @override List get props => [space]; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart index dcd44ac8..795e2c3a 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -30,7 +30,7 @@ class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { initiallyExpanded: spaceIsExpanded, onExpansionChanged: (expanded) {}, onItemSelected: () => context.read().add( - SelectSpaceEvent(space: space), + SelectSpaceEvent(community: community, space: space), ), children: space.children .map( diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..7663a3f3 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 75efc595b47b6ef4eec385e57b1dd8f26158ad08 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 16:22:11 +0300 Subject: [PATCH 9/9] reverted to old import to avoid confusion with QA team. --- lib/utils/app_routes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 7663a3f3..263bdbd6 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart';