added position elements to space

This commit is contained in:
hannathkadher
2024-10-08 11:39:10 +04:00
parent cdd22f8d76
commit 081d6fd65f
8 changed files with 342 additions and 198 deletions

View File

@ -1,4 +1,5 @@
import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/model/region_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart';
class CommunityModel { class CommunityModel {
final String uuid; final String uuid;
@ -7,6 +8,7 @@ class CommunityModel {
final String name; final String name;
final String description; final String description;
final RegionModel? region; final RegionModel? region;
List<SpaceModel> spaces;
CommunityModel({ CommunityModel({
required this.uuid, required this.uuid,
@ -14,6 +16,7 @@ class CommunityModel {
required this.updatedAt, required this.updatedAt,
required this.name, required this.name,
required this.description, required this.description,
required this.spaces,
this.region, this.region,
}); });
@ -26,6 +29,11 @@ class CommunityModel {
description: json['description'], description: json['description'],
region: region:
json['region'] != null ? RegionModel.fromJson(json['region']) : null, 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, 'name': name,
'description': description, 'description': description,
'region': region?.toJson(), 'region': region?.toJson(),
'spaces': spaces
.map((space) => space.toMap())
.toList(), // Convert spaces to Map
}; };
} }
} }

View File

@ -1,3 +1,4 @@
import 'dart:ui';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/model/community_model.dart';
class SpaceModel { class SpaceModel {
@ -11,7 +12,10 @@ class SpaceModel {
final bool isParent; final bool isParent;
final SpaceModel? parent; final SpaceModel? parent;
final CommunityModel? community; final CommunityModel? community;
final List<SpaceModel> children; // List of child spaces final List<SpaceModel> children;
final String icon;
Offset position;
bool isHovered;
SpaceModel({ SpaceModel({
required this.uuid, required this.uuid,
@ -25,6 +29,9 @@ class SpaceModel {
this.parent, this.parent,
this.community, this.community,
required this.children, required this.children,
required this.icon,
required this.position,
this.isHovered = false,
}); });
factory SpaceModel.fromJson(Map<String, dynamic> json) { factory SpaceModel.fromJson(Map<String, dynamic> json) {
@ -47,6 +54,11 @@ class SpaceModel {
.map((child) => SpaceModel.fromJson(child)) .map((child) => SpaceModel.fromJson(child))
.toList() .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(), 'parent': parent?.toMap(),
'community': community?.toMap(), 'community': community?.toMap(),
'children': children.map((child) => child.toMap()).toList(), 'children': children.map((child) => child.toMap()).toList(),
'icon': icon,
'position': {'dx': position.dx, 'dy': position.dy},
'isHovered': isHovered,
}; };
} }
} }

View File

@ -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<Connection> 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;
}
}

View File

@ -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),
),
),
);
}
}

View File

@ -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,
),
],
],
),
),
),
);
}
}

View File

@ -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,
),
),
],
),
);
}
}

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; 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/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/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_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/dialogs/create_space_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/view/sidebar_widget.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/services/space_mana_api.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
@ -41,7 +43,7 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
List<SpaceModel> spaces = await _api.getSpaceHierarchy(community.uuid); List<SpaceModel> spaces = await _api.getSpaceHierarchy(community.uuid);
// Store the result in the communitySpaces map // Store the result in the communitySpaces map
communitySpaces[community.name] = spaces; community.spaces = spaces;
} }
// Update the state to reflect loaded data // Update the state to reflect loaded data
@ -128,7 +130,8 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
child: Container( child: Container(
width: 2000, // Large canvas width: 2000, // Large canvas
height: 2000, // Large canvas height: 2000, // Large canvas
color: ColorsManager.transparentColor, // Transparent background color: ColorsManager
.transparentColor, // Transparent background
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@ -149,8 +152,51 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
children: spaces children: spaces
.asMap() .asMap()
.entries .entries
.map((entry) => _buildSpaceCard( .map((entry) => SpaceCardWidget(
entry.key, screenSize)) 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(), .toList(),
), ),
), ),
@ -224,139 +270,6 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
}, },
); );
} }
// 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 // Model for storing space information
@ -386,61 +299,3 @@ class Connection {
required this.direction}); required this.direction});
} }
// Custom painter to draw lines between connected spaces
class CurvedLinePainter extends CustomPainter {
final List<Connection> 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;
}
}

View File

@ -49,7 +49,7 @@ abstract class ColorsManager {
static const Color yaGreen = Color(0xFFFFBF44); static const Color yaGreen = Color(0xFFFFBF44);
static const Color checkBoxFillColor = Color(0xFFF3F3F3); static const Color checkBoxFillColor = Color(0xFFF3F3F3);
static const Color semiTransparentBlackColor = Color(0x3F000000); static const Color semiTransparentBlackColor = Color(0x3F000000);
static const Color transparentColor = Color(0x00000000) static const Color transparentColor = Color(0x00000000);
} }
//0036E6 //0036E6