diff --git a/lib/pages/spaces_management/model/community_model.dart b/lib/pages/spaces_management/model/community_model.dart index a37dcbd5..182aee73 100644 --- a/lib/pages/spaces_management/model/community_model.dart +++ b/lib/pages/spaces_management/model/community_model.dart @@ -1,4 +1,5 @@ import 'package:syncrow_web/pages/auth/model/region_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; class CommunityModel { final String uuid; @@ -7,6 +8,7 @@ class CommunityModel { final String name; final String description; final RegionModel? region; + List spaces; CommunityModel({ required this.uuid, @@ -14,6 +16,7 @@ class CommunityModel { required this.updatedAt, required this.name, required this.description, + required this.spaces, this.region, }); @@ -26,6 +29,11 @@ class CommunityModel { description: json['description'], region: json['region'] != null ? RegionModel.fromJson(json['region']) : null, + spaces: json['spaces'] != null + ? (json['spaces'] as List) + .map((space) => SpaceModel.fromJson(space)) + .toList() + : [], ); } @@ -37,6 +45,9 @@ class CommunityModel { 'name': name, 'description': description, 'region': region?.toJson(), + 'spaces': spaces + .map((space) => space.toMap()) + .toList(), // Convert spaces to Map }; } } diff --git a/lib/pages/spaces_management/model/space_model.dart b/lib/pages/spaces_management/model/space_model.dart index 82a94d16..4ba54545 100644 --- a/lib/pages/spaces_management/model/space_model.dart +++ b/lib/pages/spaces_management/model/space_model.dart @@ -1,3 +1,4 @@ +import 'dart:ui'; import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; class SpaceModel { @@ -11,7 +12,10 @@ class SpaceModel { final bool isParent; final SpaceModel? parent; final CommunityModel? community; - final List children; // List of child spaces + final List children; + final String icon; + Offset position; + bool isHovered; SpaceModel({ required this.uuid, @@ -25,6 +29,9 @@ class SpaceModel { this.parent, this.community, required this.children, + required this.icon, + required this.position, + this.isHovered = false, }); factory SpaceModel.fromJson(Map json) { @@ -47,6 +54,11 @@ class SpaceModel { .map((child) => SpaceModel.fromJson(child)) .toList() : [], + icon: json['icon'], // New field from JSON + position: json['position'] != null + ? Offset(json['position']['dx'], json['position']['dy']) + : Offset(0, 0), // Default position if not provided in JSON + isHovered: json['isHovered'] ?? false, // Default isHovered if not in JSON ); } @@ -63,6 +75,9 @@ class SpaceModel { 'parent': parent?.toMap(), 'community': community?.toMap(), 'children': children.map((child) => child.toMap()).toList(), + 'icon': icon, + 'position': {'dx': position.dx, 'dy': position.dy}, + 'isHovered': isHovered, }; } } diff --git a/lib/pages/spaces_management/view/curved_line_painter.dart b/lib/pages/spaces_management/view/curved_line_painter.dart new file mode 100644 index 00000000..2d6c523a --- /dev/null +++ b/lib/pages/spaces_management/view/curved_line_painter.dart @@ -0,0 +1,62 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/view/spaces_management_page.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CurvedLinePainter extends CustomPainter { + final List connections; + + CurvedLinePainter(this.connections); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = ColorsManager.blackColor + ..strokeWidth = 2 + ..style = PaintingStyle.stroke; + + // Ensure connections exist before painting + if (connections.isEmpty) { + return; // Nothing to paint if there are no connections + } + + for (var connection in connections) { + // Ensure positions are valid before drawing lines + if (connection.startSpace.position == null || + connection.endSpace.position == null) { + continue; + } + + Offset start = connection.startSpace.position + + Offset(75, 60); // Center bottom of start space + Offset end = connection.endSpace.position + + Offset(75, 0); // Center top of end space + + if (connection.direction == 'down') { + // Curved line for down connections + final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50); + final path = Path() + ..moveTo(start.dx, start.dy) + ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy); + canvas.drawPath(path, paint); + } else if (connection.direction == 'right') { + // Straight line for right connections + canvas.drawLine(start, end, paint); + } else if (connection.direction == 'left') { + // Straight line for left connections + canvas.drawLine(start, end, paint); + } + + // Draw small connection dots at the start and end points + final dotPaint = Paint()..color = ColorsManager.blackColor; + canvas.drawCircle(start, 5, dotPaint); // Start dot + canvas.drawCircle(end, 5, dotPaint); // End dot + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/pages/spaces_management/view/plus_button_widget.dart b/lib/pages/spaces_management/view/plus_button_widget.dart new file mode 100644 index 00000000..ee2faad6 --- /dev/null +++ b/lib/pages/spaces_management/view/plus_button_widget.dart @@ -0,0 +1,55 @@ +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 Size screenSize; + final Function(int index, Offset newPosition, String direction) onButtonTap; + + const PlusButtonWidget({ + Key? key, + required this.index, + required this.direction, + required this.offset, + required this.screenSize, + required this.onButtonTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + left: offset.dx, + top: offset.dy, + child: GestureDetector( + onTap: () { + Offset newPosition; + switch (direction) { + case 'left': + newPosition = Offset(-200, 0); + break; + case 'right': + newPosition = Offset(200, 0); + break; + case 'down': + newPosition = Offset(0, 150); + break; + default: + newPosition = Offset.zero; + } + onButtonTap(index, newPosition, direction); + }, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.secondaryColor, + shape: BoxShape.circle, + ), + child: const Icon(Icons.add, color: Colors.white, size: 20), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/view/space_card_widget.dart b/lib/pages/spaces_management/view/space_card_widget.dart new file mode 100644 index 00000000..ff9909c1 --- /dev/null +++ b/lib/pages/spaces_management/view/space_card_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import 'plus_button_widget.dart'; // Make sure to import your PlusButtonWidget + +class SpaceCardWidget extends StatelessWidget { + final int index; + final Size screenSize; + final Offset position; + final bool isHovered; + final Function(int index, Offset delta) onPanUpdate; + final Function(int index, bool isHovered) onHoverChanged; + final Function(int index, Offset newPosition, String direction) onButtonTap; + final Widget Function(int index) buildSpaceContainer; + + const SpaceCardWidget({ + Key? key, + required this.index, + required this.screenSize, + required this.position, + required this.isHovered, + required this.onPanUpdate, + required this.onHoverChanged, + required this.onButtonTap, + required this.buildSpaceContainer, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + left: position.dx, + top: position.dy, + child: GestureDetector( + onPanUpdate: (details) { + // Call the provided callback to update the position + onPanUpdate(index, details.delta); + }, + child: MouseRegion( + onEnter: (_) { + // Call the provided callback to handle hover state + onHoverChanged(index, true); + }, + onExit: (_) { + // Call the provided callback to handle hover state + onHoverChanged(index, false); + }, + child: Stack( + clipBehavior: Clip.none, + children: [ + buildSpaceContainer(index), // Build the space container + if (isHovered) ...[ + PlusButtonWidget( + index: index, + direction: 'left', + offset: const Offset(-21, 20), + screenSize: screenSize, + onButtonTap: onButtonTap, + ), + PlusButtonWidget( + index: index, + direction: 'right', + offset: const Offset(140, 20), + screenSize: screenSize, + onButtonTap: onButtonTap, + ), + PlusButtonWidget( + index: index, + direction: 'down', + offset: const Offset(63, 50), + screenSize: screenSize, + onButtonTap: onButtonTap, + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/view/space_container_widget.dart b/lib/pages/spaces_management/view/space_container_widget.dart new file mode 100644 index 00000000..d7a6ee14 --- /dev/null +++ b/lib/pages/spaces_management/view/space_container_widget.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + + +class SpaceContainerWidget extends StatelessWidget { + final int index; + final String icon; + final String name; + + const SpaceContainerWidget({ + Key? key, + required this.index, + required this.icon, + required this.name, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: 150, + height: 60, + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), // shadow position + ), + ], + ), + child: Row( + children: [ + Container( + width: 40, + height: 60, + decoration: const BoxDecoration( + color: ColorsManager.secondaryColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + child: Center( + child: SvgPicture.asset( + icon, + width: 24, + height: 24, + ), + ), + ), + const SizedBox(width: 10), + Text( + name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/spaces_management/view/spaces_management_page.dart b/lib/pages/spaces_management/view/spaces_management_page.dart index 841e3302..026e9d50 100644 --- a/lib/pages/spaces_management/view/spaces_management_page.dart +++ b/lib/pages/spaces_management/view/spaces_management_page.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/common/buttons/add_space_button.dart'; import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/view/curved_line_painter.dart'; import 'package:syncrow_web/pages/spaces_management/view/dialogs/create_space_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/view/sidebar_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/view/space_card_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/view/space_container_widget.dart'; import 'package:syncrow_web/services/space_mana_api.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -41,7 +43,7 @@ class SpaceManagementPageState extends State { List spaces = await _api.getSpaceHierarchy(community.uuid); // Store the result in the communitySpaces map - communitySpaces[community.name] = spaces; + community.spaces = spaces; } // Update the state to reflect loaded data @@ -128,7 +130,8 @@ class SpaceManagementPageState extends State { child: Container( width: 2000, // Large canvas height: 2000, // Large canvas - color: ColorsManager.transparentColor, // Transparent background + color: ColorsManager + .transparentColor, // Transparent background child: Stack( clipBehavior: Clip.none, children: [ @@ -149,8 +152,51 @@ class SpaceManagementPageState extends State { children: spaces .asMap() .entries - .map((entry) => _buildSpaceCard( - entry.key, screenSize)) + .map((entry) => SpaceCardWidget( + index: entry.key, + screenSize: screenSize, + position: spaces[entry.key] + .position, + isHovered: spaces[entry.key] + .isHovered, + onPanUpdate: (int index, + Offset delta) { + setState(() { + spaces[index] + .position += delta; + }); + }, + onHoverChanged: (int index, + bool isHovered) { + setState(() { + spaces[index] + .isHovered = + isHovered; + }); + }, + onButtonTap: (int index, + Offset newPosition, + String direction) { + _showCreateSpaceDialog( + screenSize, + position: spaces[index] + .position + + newPosition, + parentIndex: index, + direction: direction, + ); + }, + buildSpaceContainer: + (int index) { + return SpaceContainerWidget( + index: index, + icon: + spaces[index].icon, + name: + spaces[index].name, + ); + }, + )) .toList(), ), ), @@ -224,139 +270,6 @@ class SpaceManagementPageState extends State { }, ); } - - // Function to build a draggable space card - Widget _buildSpaceCard(int index, Size screenSize) { - return Positioned( - left: spaces[index].position.dx, - top: spaces[index].position.dy, - child: GestureDetector( - onPanUpdate: (details) { - // Update the position of the space card while dragging - setState(() { - spaces[index].position += details.delta; - }); - }, - child: MouseRegion( - onEnter: (_) { - // Show plus buttons on hover - setState(() { - spaces[index].isHovered = true; - }); - }, - onExit: (_) { - // Hide plus buttons when not hovered - setState(() { - spaces[index].isHovered = false; - }); - }, - child: Stack( - clipBehavior: Clip.none, - children: [ - _buildSpaceContainer(index), - if (spaces[index].isHovered) ...[ - _buildPlusButton( - index, 'left', const Offset(-21, 20), screenSize), - _buildPlusButton( - index, 'right', const Offset(140, 20), screenSize), - _buildPlusButton( - index, 'down', const Offset(63, 50), screenSize), - ], - ], - ), - ), - ), - ); - } - - // Function to build the space container with the styled format - Widget _buildSpaceContainer(int index) { - return Container( - width: 150, - height: 60, - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: const Offset(0, 3), - ), - ], - ), - child: Row( - children: [ - Container( - width: 40, - height: 60, - decoration: const BoxDecoration( - color: ColorsManager.secondaryColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(15), - bottomLeft: Radius.circular(15), - ), - ), - child: Center( - child: SvgPicture.asset( - spaces[index].icon, - width: 24, - height: 24, - ), - ), - ), - const SizedBox(width: 10), - Text( - spaces[index].name, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - ); - } - - // Function to build plus buttons for new space creation - Widget _buildPlusButton( - int index, String direction, Offset offset, Size screenSize) { - return Positioned( - left: offset.dx, - top: offset.dy, - child: GestureDetector( - onTap: () { - Offset newPosition; - switch (direction) { - case 'left': - newPosition = spaces[index].position + const Offset(-200, 0); - break; - case 'right': - newPosition = spaces[index].position + const Offset(200, 0); - break; - case 'down': - newPosition = spaces[index].position + const Offset(0, 150); - break; - default: - newPosition = spaces[index].position; - } - // Open the dialog to create a new space and pass down the new position - _showCreateSpaceDialog(screenSize, - position: newPosition, parentIndex: index, direction: direction); - }, - child: Container( - width: 30, - height: 30, - decoration: const BoxDecoration( - color: ColorsManager.secondaryColor, - shape: BoxShape.circle, - ), - child: const Icon(Icons.add, color: Colors.white, size: 20), - ), - ), - ); - } } // Model for storing space information @@ -386,61 +299,3 @@ class Connection { required this.direction}); } -// Custom painter to draw lines between connected spaces - -class CurvedLinePainter extends CustomPainter { - final List connections; - - CurvedLinePainter(this.connections); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = ColorsManager.blackColor - ..strokeWidth = 2 - ..style = PaintingStyle.stroke; - - // Ensure connections exist before painting - if (connections.isEmpty) { - return; // Nothing to paint if there are no connections - } - - for (var connection in connections) { - // Ensure positions are valid before drawing lines - if (connection.startSpace.position == null || - connection.endSpace.position == null) { - continue; - } - - Offset start = connection.startSpace.position + - Offset(75, 60); // Center bottom of start space - Offset end = connection.endSpace.position + - Offset(75, 0); // Center top of end space - - if (connection.direction == 'down') { - // Curved line for down connections - final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50); - final path = Path() - ..moveTo(start.dx, start.dy) - ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy); - canvas.drawPath(path, paint); - } else if (connection.direction == 'right') { - // Straight line for right connections - canvas.drawLine(start, end, paint); - } else if (connection.direction == 'left') { - // Straight line for left connections - canvas.drawLine(start, end, paint); - } - - // Draw small connection dots at the start and end points - final dotPaint = Paint()..color = ColorsManager.blackColor; - canvas.drawCircle(start, 5, dotPaint); // Start dot - canvas.drawCircle(end, 5, dotPaint); // End dot - } - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; - } -} diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 6e54ba0e..e6dc4690 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -49,7 +49,7 @@ abstract class ColorsManager { static const Color yaGreen = Color(0xFFFFBF44); static const Color checkBoxFillColor = Color(0xFFF3F3F3); static const Color semiTransparentBlackColor = Color(0x3F000000); - static const Color transparentColor = Color(0x00000000) + static const Color transparentColor = Color(0x00000000); } //0036E6 \ No newline at end of file