mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
fixed space layout
This commit is contained in:
@ -19,6 +19,7 @@ import 'package:syncrow_web/pages/spaces_management/widgets/space_container_widg
|
|||||||
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';
|
||||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
|
||||||
class SpaceManagementPage extends StatefulWidget {
|
class SpaceManagementPage extends StatefulWidget {
|
||||||
const SpaceManagementPage({super.key});
|
const SpaceManagementPage({super.key});
|
||||||
@ -28,75 +29,44 @@ class SpaceManagementPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SpaceManagementPageState extends State<SpaceManagementPage> {
|
class SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||||
// Store created spaces
|
|
||||||
List<SpaceData> spaces = [];
|
|
||||||
List<Connection> connections = [];
|
|
||||||
double canvasWidth = 1000; // Initial canvas width
|
|
||||||
double canvasHeight = 1000; // Initial canvas height
|
|
||||||
|
|
||||||
// Track whether to show the community list view or community structure
|
|
||||||
|
|
||||||
// Selected community
|
|
||||||
CommunityModel? selectedCommunity;
|
CommunityModel? selectedCommunity;
|
||||||
|
|
||||||
// API instance
|
|
||||||
final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi();
|
final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi();
|
||||||
|
|
||||||
// Data structure to store community and associated spaces
|
|
||||||
Map<String, List<SpaceModel>> communitySpaces = {};
|
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)),
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateCanvasSize() {
|
|
||||||
double maxX = 0;
|
|
||||||
double maxY = 0;
|
|
||||||
|
|
||||||
// Calculate the maximum X and Y positions of all spaces
|
|
||||||
for (var space in spaces) {
|
|
||||||
maxX = max(maxX, space.position.dx + 150); // Add width of space
|
|
||||||
maxY = max(maxY, space.position.dy + 60); // Add height of space
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add padding (but avoid adding arbitrary amounts like 1000)
|
|
||||||
double newWidth =
|
|
||||||
max(maxX + 500, canvasWidth); // Use max to ensure the canvas only grows
|
|
||||||
double newHeight = max(maxY + 500, canvasHeight);
|
|
||||||
|
|
||||||
// Set the new canvas size dynamically
|
|
||||||
setState(() {
|
|
||||||
canvasWidth = newWidth;
|
|
||||||
canvasHeight = newHeight;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Size screenSize = MediaQuery.of(context).size;
|
Size screenSize = MediaQuery.of(context).size;
|
||||||
|
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => SpaceManagementBloc(CommunitySpaceManagementApi())
|
create: (context) => SpaceManagementBloc(CommunitySpaceManagementApi())
|
||||||
..add(LoadCommunityAndSpacesEvent()),
|
..add(LoadCommunityAndSpacesEvent()),
|
||||||
child: WebScaffold(
|
child: WebScaffold(
|
||||||
appBarTitle: Text(
|
appBarTitle: Text('Space Management',
|
||||||
'Space Management',
|
style: Theme.of(context).textTheme.headlineLarge),
|
||||||
style: Theme.of(context).textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
enableMenuSidebar: false,
|
enableMenuSidebar: false,
|
||||||
scaffoldBody: BlocBuilder<SpaceManagementBloc, SpaceManagementState>(
|
scaffoldBody: BlocBuilder<SpaceManagementBloc, SpaceManagementState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is SpaceManagementLoading) {
|
if (state is SpaceManagementLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
} else if (state is SpaceManagementLoaded) {
|
} else if (state is SpaceManagementLoaded) {
|
||||||
return _buildLoadedState(context, screenSize, state.communities);
|
return _buildLoadedState(context, screenSize, state.communities);
|
||||||
} else if (state is SpaceManagementError) {
|
} else if (state is SpaceManagementError) {
|
||||||
return Center(child: Text('Error: ${state.errorMessage}'));
|
return Center(child: Text('Error: ${state.errorMessage}'));
|
||||||
}
|
}
|
||||||
return Container();
|
return Container();
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -126,166 +96,271 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
|
|||||||
|
|
||||||
Widget _buildCommunityStructureArea(BuildContext context, Size screenSize) {
|
Widget _buildCommunityStructureArea(BuildContext context, Size screenSize) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
left: BorderSide(
|
left: BorderSide(
|
||||||
color: ColorsManager.whiteColors,
|
color: ColorsManager.whiteColors,
|
||||||
width: 1.0), // Light left border to match
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Background color for canvas
|
Flexible(
|
||||||
child: Column(
|
child: InteractiveViewer(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
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: [
|
children: [
|
||||||
|
// Main node container
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 27.0),
|
padding: EdgeInsets.all(8),
|
||||||
width: double.infinity,
|
width: 150,
|
||||||
|
height: 60,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ColorsManager.whiteColors,
|
color: ColorsManager.whiteColors,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: ColorsManager.shadowBlackColor, // Subtle shadow
|
color: Colors.grey.withOpacity(0.5),
|
||||||
spreadRadius: 0, // No spread
|
spreadRadius: 2,
|
||||||
blurRadius: 8, // Softer shadow edges
|
blurRadius: 5,
|
||||||
offset: const Offset(0, 4), // Shadow only on the bottom
|
offset: const Offset(0, 3), // shadow position
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
widget.data.id,
|
||||||
children: [
|
style: TextStyle(color: Colors.white),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Use Expanded to ensure InteractiveViewer takes the available space
|
if (isHovered) ...[
|
||||||
Flexible(
|
// Add icon on the right
|
||||||
child: InteractiveViewer(
|
Positioned(
|
||||||
boundaryMargin: const EdgeInsets.all(20000), // Adjusted to 500
|
right: -20,
|
||||||
minScale: 0.5, // Minimum zoom scale
|
child: IconButton(
|
||||||
maxScale: 5.5, // Maximum zoom scale
|
icon: Icon(Icons.add_circle, color: Colors.green, size: 20),
|
||||||
panEnabled: true, // Enable panning
|
onPressed: () => widget.onAddNode("right"),
|
||||||
scaleEnabled: true, // Enable zooming
|
),
|
||||||
child: Container(
|
|
||||||
width: canvasWidth, // Large width for free movement
|
|
||||||
height: canvasHeight, // Large height for free movement
|
|
||||||
child: spaces.isEmpty
|
|
||||||
? Center(
|
|
||||||
child: AddSpaceButton(
|
|
||||||
onTap: () {
|
|
||||||
_showCreateSpaceDialog(screenSize);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
CustomPaint(
|
|
||||||
size: const Size(4000, 4000),
|
|
||||||
painter: CurvedLinePainter(connections),
|
|
||||||
),
|
|
||||||
...spaces.asMap().entries.map((entry) {
|
|
||||||
final space = entry.value;
|
|
||||||
return Positioned(
|
|
||||||
left: space.position.dx,
|
|
||||||
top: space.position.dy,
|
|
||||||
child: SpaceCardWidget(
|
|
||||||
index: entry.key,
|
|
||||||
screenSize: screenSize,
|
|
||||||
position: space.position,
|
|
||||||
isHovered: space.isHovered,
|
|
||||||
onPanUpdate: (int index, Offset delta) {
|
|
||||||
setState(() {
|
|
||||||
spaces[index].position += delta;
|
|
||||||
});
|
|
||||||
updateCanvasSize();
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
),
|
// 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"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
updateCanvasSize();
|
|
||||||
|
|
||||||
// Add connection for down-button
|
|
||||||
if (parentIndex != null && direction != null) {
|
|
||||||
connections.add(Connection(
|
|
||||||
startSpace: spaces[parentIndex],
|
|
||||||
endSpace: newSpace,
|
|
||||||
direction: direction,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@ -63,6 +63,7 @@ class SpaceContainerWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user