Fixed space layout

This commit is contained in:
hannathkadher
2024-11-15 00:38:14 +04:00
parent eacee98de2
commit de57e0f21d
7 changed files with 292 additions and 306 deletions

View File

@ -1,8 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/add_space_button.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/bloc/space_management_state.dart';
@ -10,16 +7,12 @@ import 'package:syncrow_web/pages/spaces_management/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_data_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/widgets/community_structure_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/gradient_canvas_border_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/loaded_space_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/sidebar_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/space_container_widget.dart';
import 'package:syncrow_web/services/space_mana_api.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
import 'package:fl_chart/fl_chart.dart';
class SpaceManagementPage extends StatefulWidget {
const SpaceManagementPage({super.key});
@ -34,12 +27,8 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
Map<String, List<SpaceModel>> communitySpaces = {};
double canvasWidth = 1000;
double canvasHeight = 1000;
final List<NodeData> nodes = [
NodeData(id: 'Node 1', position: Offset(100, 100)),
NodeData(id: 'Node 2', position: Offset(300, 300)),
NodeData(id: 'Node 3', position: Offset(500, 500)),
];
List<SpaceData> spaces = [];
List<Connection> connections = [];
@override
void initState() {
@ -48,7 +37,6 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return BlocProvider(
create: (context) => SpaceManagementBloc(CommunitySpaceManagementApi())
..add(LoadCommunityAndSpacesEvent()),
@ -61,7 +49,15 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
if (state is SpaceManagementLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is SpaceManagementLoaded) {
return _buildLoadedState(context, screenSize, state.communities);
return LoadedSpaceView(
communities: state.communities,
selectedCommunity: selectedCommunity,
onCommunitySelected: (community) {
setState(() {
selectedCommunity = community;
});
},
);
} else if (state is SpaceManagementError) {
return Center(child: Text('Error: ${state.errorMessage}'));
}
@ -72,7 +68,7 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
}
Widget _buildLoadedState(
BuildContext context, Size screenSize, List<CommunityModel> communities) {
BuildContext context, List<CommunityModel> communities) {
return Stack(
clipBehavior: Clip.none,
children: [
@ -82,285 +78,15 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
communities: communities,
onCommunitySelected: (community) {
setState(() {
selectedCommunity = community; // Set the selected community
selectedCommunity = community;
});
},
),
_buildCommunityStructureArea(context, screenSize)
CommunityStructureArea(selectedCommunity: selectedCommunity),
],
),
const GradientCanvasBorderWidget()
],
);
}
Widget _buildCommunityStructureArea(BuildContext context, Size screenSize) {
return Expanded(
child: Container(
decoration: const BoxDecoration(
border: Border(
left: BorderSide(
color: ColorsManager.whiteColors,
width: 1.0), // Light left border to match
),
),
// Background color for canvas
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 27.0),
width: double.infinity,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.shadowBlackColor, // Subtle shadow
spreadRadius: 0, // No spread
blurRadius: 8, // Softer shadow edges
offset: const Offset(0, 4), // Shadow only on the bottom
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Community Structure',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
if (selectedCommunity != null) ...[
Text(
selectedCommunity!.name, // Show community name
style: const TextStyle(
fontSize: 16,
color: ColorsManager.blackColor,
),
),
],
],
),
),
Flexible(
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(500), // Adjusted for smoother panning
minScale: 0.5,
maxScale: 3.0,
constrained: false,
child: Container(
width: canvasWidth,
height: canvasHeight,
child: Stack(
children: [
// Draw connections between nodes
for (int i = 0; i < nodes.length - 1; i++)
CustomPaint(
painter:
EdgePainter(nodes[i].position, nodes[i + 1].position),
),
// Render each node and make it draggable
for (var node in nodes)
Positioned(
left: node.position.dx,
top: node.position.dy,
child: DraggableNode(
data: node,
onAddNode: (direction) => _addNode(node, direction),
onPositionChanged: (newPosition) {
_updateNodePosition(node, newPosition);
},
),
),
],
),
),
))
]),
));
}
void _updateNodePosition(NodeData node, Offset newPosition) {
setState(() {
node.position = newPosition;
// Expand canvas to the right when node approaches the right edge
if (node.position.dx >= canvasWidth - 200) {
canvasWidth += 200;
print("Canvas width expanded to $canvasWidth");
}
// Expand canvas downward when node approaches the bottom edge
if (node.position.dy >= canvasHeight - 200) {
canvasHeight += 200;
print("Canvas height expanded to $canvasHeight");
}
// Expand canvas to the left when node approaches the left edge
if (node.position.dx <= 200) {
double shiftAmount = 200;
canvasWidth += shiftAmount;
// Shift all nodes to the right by shiftAmount
for (var n in nodes) {
n.position = Offset(n.position.dx + shiftAmount, n.position.dy);
}
print("Canvas expanded to the left. New width: $canvasWidth");
}
// Prevent nodes from going out of bounds on top edge
if (node.position.dy < 0) {
node.position = Offset(node.position.dx, 0);
}
// Log the current canvas size for debugging
print(
"Current canvas size: width = $canvasWidth, height = $canvasHeight");
});
}
void _addNode(NodeData parent, String direction) {
Offset newPosition;
switch (direction) {
case "right":
newPosition = parent.position + Offset(200, 0);
break;
case "left":
newPosition = parent.position - Offset(200, 0);
break;
case "bottom":
newPosition = parent.position + Offset(0, 200);
break;
default:
return;
}
setState(() {
nodes
.add(NodeData(id: 'Node ${nodes.length + 1}', position: newPosition));
// Expand the canvas if necessary
if (newPosition.dx >= canvasWidth - 200) canvasWidth += 200;
if (newPosition.dy >= canvasHeight - 200) canvasHeight += 200;
if (newPosition.dx < 0) canvasWidth += 200;
if (newPosition.dy < 0) canvasHeight += 200;
print("New node added in direction $direction at $newPosition");
});
}
}
class NodeData {
String id;
Offset position;
NodeData({required this.id, required this.position});
}
class DraggableNode extends StatefulWidget {
final NodeData data;
final ValueChanged<Offset> onPositionChanged;
final ValueChanged<String>
onAddNode; // Callback for adding a node in a specific direction
DraggableNode({
required this.data,
required this.onPositionChanged,
required this.onAddNode,
});
@override
_DraggableNodeState createState() => _DraggableNodeState();
}
class _DraggableNodeState extends State<DraggableNode> {
bool isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: GestureDetector(
onPanUpdate: (details) {
final newPosition = widget.data.position + details.delta;
widget.onPositionChanged(newPosition);
},
child: Stack(
alignment: Alignment.center,
children: [
// Main node container
Container(
padding: EdgeInsets.all(8),
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: Text(
widget.data.id,
style: TextStyle(color: Colors.white),
),
),
if (isHovered) ...[
// Add icon on the right
Positioned(
right: -20,
child: IconButton(
icon: Icon(Icons.add_circle, color: Colors.green, size: 20),
onPressed: () => widget.onAddNode("right"),
),
),
// Add icon on the left
Positioned(
left: -20,
child: IconButton(
icon: Icon(Icons.add_circle, color: Colors.green, size: 20),
onPressed: () => widget.onAddNode("left"),
),
),
// Add icon on the bottom
Positioned(
bottom: -20,
child: IconButton(
icon: Icon(Icons.add_circle, color: Colors.green, size: 20),
onPressed: () => widget.onAddNode("bottom"),
),
),
],
],
),
),
);
}
}
class EdgePainter extends CustomPainter {
final Offset start;
final Offset end;
EdgePainter(this.start, this.end);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
canvas.drawLine(start, end, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}

View File

@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/buttons/add_space_button.dart';
import 'package:syncrow_web/pages/spaces_management/view/dialogs/create_space_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/curved_line_painter.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/space_data_model.dart';
import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/space_container_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CommunityStructureArea extends StatefulWidget {
final CommunityModel? selectedCommunity;
CommunityStructureArea({this.selectedCommunity});
@override
_CommunityStructureAreaState createState() => _CommunityStructureAreaState();
}
class _CommunityStructureAreaState extends State<CommunityStructureArea> {
double canvasWidth = 1000;
double canvasHeight = 1000;
List<SpaceData> spaces = [];
List<Connection> connections = [];
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return Expanded(
child: Container(
decoration: const BoxDecoration(
border: Border(
left: BorderSide(color: ColorsManager.whiteColors, width: 1.0),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
Flexible(
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(500),
minScale: 0.5,
maxScale: 3.0,
constrained: false,
child: Container(
width: canvasWidth,
height: canvasHeight,
child: spaces.isEmpty
? Center(
child: AddSpaceButton(
onTap: () {
_showCreateSpaceDialog(screenSize);
},
),
)
: Stack(
children: [
for (var connection in connections)
CustomPaint(
painter: CurvedLinePainter([connection])),
for (var entry in spaces.asMap().entries)
Positioned(
left: entry.value.position.dx,
top: entry.value.position.dy,
child: SpaceCardWidget(
index: entry.key,
onButtonTap: (int index, Offset newPosition,
String direction) {
_showCreateSpaceDialog(
screenSize,
position:
spaces[index].position + newPosition,
parentIndex: index,
direction: direction,
);
},
position: entry.value.position,
isHovered: entry.value.isHovered,
screenSize: screenSize,
onHoverChanged: _handleHoverChanged,
onPositionChanged: (newPosition) {
_updateNodePosition(
entry.value, newPosition);
},
buildSpaceContainer: (int index) {
return SpaceContainerWidget(
index: index,
icon: spaces[index].icon,
name: spaces[index].name,
);
},
),
),
],
),
),
),
),
],
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 27.0),
width: double.infinity,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.shadowBlackColor,
spreadRadius: 0,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Community Structure',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
if (widget.selectedCommunity != null)
Text(
widget.selectedCommunity!.name,
style: const TextStyle(
fontSize: 16, color: ColorsManager.blackColor),
),
],
),
);
}
void _updateNodePosition(SpaceData node, Offset newPosition) {
setState(() {
node.position = newPosition;
// Expand canvas to the right when node approaches the right edge
if (node.position.dx >= canvasWidth - 200) {
canvasWidth += 200;
}
// Expand canvas downward when node approaches the bottom edge
if (node.position.dy >= canvasHeight - 200) {
canvasHeight += 200;
}
// Expand canvas to the left when node approaches the left edge
if (node.position.dx <= 200) {
double shiftAmount = 200;
canvasWidth += shiftAmount;
// Shift all nodes to the right by shiftAmount
for (var n in spaces) {
n.position = Offset(n.position.dx + shiftAmount, n.position.dy);
}
}
// Prevent nodes from going out of bounds on top edge
if (node.position.dy < 0) {
node.position = Offset(node.position.dx, 0);
}
});
}
void _showCreateSpaceDialog(Size screenSize,
{Offset? position, int? parentIndex, String? direction}) {
showDialog(
context: context,
builder: (BuildContext context) {
return CreateSpaceDialog(
onCreateSpace: (String name, String icon) {
setState(() {
// Set the first space in the center or use passed position
Offset centerPosition = position ??
Offset(
screenSize.width / 2 - 75, // Center horizontally
screenSize.height / 2 -
100, // Slightly above the center vertically
);
SpaceData newSpace =
SpaceData(name: name, icon: icon, position: centerPosition);
spaces.add(newSpace);
_updateNodePosition(newSpace, newSpace.position);
// Add connection for down-button
if (parentIndex != null && direction != null) {
SpaceData parentSpace = spaces[parentIndex];
connections.add(Connection(
startSpace: parentSpace,
endSpace: newSpace,
direction: direction,
));
}
});
},
);
},
);
}
void _handleHoverChanged(int index, bool isHovered) {
setState(() {
spaces[index].isHovered = isHovered;
});
}
}

View File

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/model/connection_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
@ -40,14 +38,20 @@ class CurvedLinePainter extends CustomPainter {
..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy);
canvas.drawPath(path, paint);
} else if (connection.direction == 'right') {
// Straight line for right connections
start = connection.startSpace.position +
const Offset(150, 30); // Right center
end = connection.endSpace.position + const Offset(0, 30); // Left center
canvas.drawLine(start, end, paint);
} else if (connection.direction == 'left') {
// Straight line for left connections
start =
connection.startSpace.position + const Offset(0, 30); // Left center
end = connection.endSpace.position +
const Offset(150, 30); // Right center
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

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/community_structure_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/gradient_canvas_border_widget.dart';
import 'package:syncrow_web/pages/spaces_management/widgets/sidebar_widget.dart';
class LoadedSpaceView extends StatefulWidget {
final List<CommunityModel> communities;
final CommunityModel? selectedCommunity;
final ValueChanged<CommunityModel> onCommunitySelected;
const LoadedSpaceView({
Key? key,
required this.communities,
this.selectedCommunity,
required this.onCommunitySelected,
}) : super(key: key);
@override
_LoadedStateViewState createState() => _LoadedStateViewState();
}
class _LoadedStateViewState extends State<LoadedSpaceView> {
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
Row(
children: [
SidebarWidget(
communities: widget.communities,
onCommunitySelected: widget.onCommunitySelected,
),
CommunityStructureArea(selectedCommunity: widget.selectedCommunity),
],
),
const GradientCanvasBorderWidget(),
],
);
}
}

View File

@ -5,7 +5,6 @@ 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({
@ -13,7 +12,6 @@ class PlusButtonWidget extends StatelessWidget {
required this.index,
required this.direction,
required this.offset,
required this.screenSize,
required this.onButtonTap,
});

View File

@ -6,18 +6,18 @@ class SpaceCardWidget extends StatelessWidget {
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;
final ValueChanged<Offset> onPositionChanged;
const SpaceCardWidget({
super.key,
required this.index,
required this.onPositionChanged,
required this.screenSize,
required this.position,
required this.isHovered,
required this.onPanUpdate,
required this.onHoverChanged,
required this.onButtonTap,
required this.buildSpaceContainer,
@ -29,7 +29,8 @@ class SpaceCardWidget extends StatelessWidget {
behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
// Call the provided callback to update the position
onPanUpdate(index, details.delta);
final newPosition = position + details.delta;
onPositionChanged(newPosition);
},
child: MouseRegion(
onEnter: (_) {
@ -41,7 +42,8 @@ class SpaceCardWidget extends StatelessWidget {
onHoverChanged(index, false);
},
child: Stack(
clipBehavior: Clip.none, // Allow hovering elements to be displayed outside the boundary
clipBehavior: Clip
.none, // Allow hovering elements to be displayed outside the boundary
children: [
buildSpaceContainer(index), // Build the space container
if (isHovered) ...[
@ -49,21 +51,18 @@ class SpaceCardWidget extends StatelessWidget {
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

@ -19,7 +19,8 @@ class SpaceWidget extends StatelessWidget {
top: position.dy,
child: GestureDetector(
onTap: onTap,
child: Container(
child:
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.white,
@ -41,6 +42,7 @@ class SpaceWidget extends StatelessWidget {
],
),
),
),
);
}