mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
530 lines
18 KiB
Dart
530 lines
18 KiB
Dart
// Flutter imports
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
// Syncrow project imports
|
|
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/model/product_model.dart';
|
|
import 'package:syncrow_web/pages/spaces_management/model/selected_product_model.dart';
|
|
import 'package:syncrow_web/pages/spaces_management/model/space_model.dart';
|
|
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/widgets/blank_community_widget.dart';
|
|
import 'package:syncrow_web/pages/spaces_management/widgets/community_structure_header_widget.dart';
|
|
import 'package:syncrow_web/pages/spaces_management/widgets/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/widgets/space_container_widget.dart';
|
|
import 'package:syncrow_web/utils/color_manager.dart';
|
|
|
|
class CommunityStructureArea extends StatefulWidget {
|
|
final CommunityModel? selectedCommunity;
|
|
SpaceModel? selectedSpace;
|
|
final List<ProductModel>? products;
|
|
final ValueChanged<SpaceModel?>? onSpaceSelected;
|
|
final List<CommunityModel> communities;
|
|
final List<SpaceModel> spaces;
|
|
|
|
CommunityStructureArea({
|
|
this.selectedCommunity,
|
|
this.selectedSpace,
|
|
required this.communities,
|
|
this.products,
|
|
required this.spaces,
|
|
this.onSpaceSelected,
|
|
});
|
|
|
|
@override
|
|
_CommunityStructureAreaState createState() => _CommunityStructureAreaState();
|
|
}
|
|
|
|
class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
|
double canvasWidth = 1000;
|
|
double canvasHeight = 1000;
|
|
List<SpaceModel> spaces = [];
|
|
List<Connection> connections = [];
|
|
late TextEditingController _nameController;
|
|
bool isEditingName = false;
|
|
late TransformationController _transformationController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
|
|
connections =
|
|
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
|
|
_adjustCanvasSizeForSpaces();
|
|
_nameController = TextEditingController(
|
|
text: widget.selectedCommunity?.name ?? '',
|
|
);
|
|
_transformationController = TransformationController();
|
|
if (widget.selectedSpace != null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_moveToSpace(widget.selectedSpace!);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
_transformationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant CommunityStructureArea oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
if (oldWidget.spaces != widget.spaces) {
|
|
setState(() {
|
|
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
|
|
connections =
|
|
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
|
|
_adjustCanvasSizeForSpaces();
|
|
});
|
|
}
|
|
|
|
if (widget.selectedSpace != oldWidget.selectedSpace &&
|
|
widget.selectedSpace != null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_moveToSpace(widget.selectedSpace!);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.selectedCommunity == null) {
|
|
return BlankCommunityWidget(
|
|
communities: widget.communities,
|
|
);
|
|
}
|
|
|
|
Size screenSize = MediaQuery.of(context).size;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
_deselectSpace(context);
|
|
},
|
|
child: Container(
|
|
decoration: const BoxDecoration(
|
|
border: Border(
|
|
left: BorderSide(color: ColorsManager.whiteColors, width: 1.0),
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
CommunityStructureHeader(
|
|
communityName: widget.selectedCommunity?.name,
|
|
isSave: isSave(spaces),
|
|
isEditingName: isEditingName,
|
|
nameController: _nameController,
|
|
onSave: _saveSpaces,
|
|
onDelete: _onDelete,
|
|
onEditName: () {
|
|
setState(() {
|
|
isEditingName = !isEditingName;
|
|
if (isEditingName) {
|
|
_nameController.text = widget.selectedCommunity?.name ?? '';
|
|
}
|
|
});
|
|
},
|
|
onNameSubmitted: (value) {
|
|
context.read<SpaceManagementBloc>().add(
|
|
UpdateCommunityEvent(
|
|
communityUuid: widget.selectedCommunity!.uuid,
|
|
name: value,
|
|
),
|
|
);
|
|
setState(() {
|
|
widget.selectedCommunity?.name = value;
|
|
isEditingName = false;
|
|
});
|
|
},
|
|
),
|
|
Flexible(
|
|
child: Stack(
|
|
children: [
|
|
InteractiveViewer(
|
|
transformationController: _transformationController,
|
|
boundaryMargin: EdgeInsets.all(500),
|
|
minScale: 0.5,
|
|
maxScale: 3.0,
|
|
constrained: false,
|
|
child: Container(
|
|
width: canvasWidth,
|
|
height: canvasHeight,
|
|
child: Stack(
|
|
children: [
|
|
for (var connection in connections)
|
|
Opacity(
|
|
opacity: _isHighlightedConnection(connection)
|
|
? 1.0
|
|
: 0.3, // Adjust opacity
|
|
child: CustomPaint(
|
|
painter: CurvedLinePainter([connection])),
|
|
),
|
|
for (var entry in spaces.asMap().entries)
|
|
if (entry.value.status != SpaceStatus.deleted)
|
|
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) {
|
|
final bool isHighlighted =
|
|
_isHighlightedSpace(spaces[index]);
|
|
|
|
return Opacity(
|
|
opacity: isHighlighted ? 1.0 : 0.3,
|
|
child: SpaceContainerWidget(
|
|
index: index,
|
|
onDoubleTap: () {
|
|
_showEditSpaceDialog(spaces[index]);
|
|
},
|
|
onTap: () {
|
|
_selectSpace(context, spaces[index]);
|
|
},
|
|
icon: spaces[index].icon ?? '',
|
|
name: spaces[index].name,
|
|
));
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (spaces.isEmpty)
|
|
Center(
|
|
child: AddSpaceButton(
|
|
onTap: () {
|
|
_showCreateSpaceDialog(screenSize,
|
|
canvasHeight: canvasHeight,
|
|
canvasWidth: canvasWidth);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
));
|
|
}
|
|
|
|
void _updateNodePosition(SpaceModel node, Offset newPosition) {
|
|
setState(() {
|
|
node.position = newPosition;
|
|
if (node.status != SpaceStatus.newSpace) {
|
|
node.status = SpaceStatus.modified; // Mark as modified
|
|
}
|
|
if (node.position.dx >= canvasWidth - 200) {
|
|
canvasWidth += 200;
|
|
}
|
|
if (node.position.dy >= canvasHeight - 200) {
|
|
canvasHeight += 200;
|
|
}
|
|
if (node.position.dx <= 200) {
|
|
double shiftAmount = 200;
|
|
canvasWidth += shiftAmount;
|
|
for (var n in spaces) {
|
|
n.position = Offset(n.position.dx + shiftAmount, n.position.dy);
|
|
}
|
|
}
|
|
if (node.position.dy < 0) {
|
|
node.position = Offset(node.position.dx, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _adjustCanvasSizeForSpaces() {
|
|
for (var space in spaces) {
|
|
if (space.position.dx >= canvasWidth - 200) {
|
|
canvasWidth = space.position.dx + 200;
|
|
}
|
|
|
|
if (space.position.dy >= canvasHeight - 200) {
|
|
canvasHeight = space.position.dy + 200;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showCreateSpaceDialog(Size screenSize,
|
|
{Offset? position,
|
|
int? parentIndex,
|
|
String? direction,
|
|
double? canvasWidth,
|
|
double? canvasHeight}) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return CreateSpaceDialog(
|
|
products: widget.products,
|
|
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
|
|
onCreateSpace: (String name, String icon,
|
|
List<SelectedProduct> selectedProducts) {
|
|
setState(() {
|
|
// Set the first space in the center or use passed position
|
|
|
|
Offset centerPosition =
|
|
position ?? _getCenterPosition(screenSize);
|
|
SpaceModel newSpace = SpaceModel(
|
|
name: name,
|
|
icon: icon,
|
|
position: centerPosition,
|
|
isPrivate: false,
|
|
children: [],
|
|
status: SpaceStatus.newSpace,
|
|
selectedProducts: selectedProducts);
|
|
|
|
if (parentIndex != null && direction != null) {
|
|
SpaceModel parentSpace = spaces[parentIndex];
|
|
parentSpace.internalId = spaces[parentIndex].internalId;
|
|
newSpace.parent = parentSpace;
|
|
final newConnection = Connection(
|
|
startSpace: parentSpace,
|
|
endSpace: newSpace,
|
|
direction: direction,
|
|
);
|
|
connections.add(newConnection);
|
|
newSpace.incomingConnection = newConnection;
|
|
parentSpace.addOutgoingConnection(newConnection);
|
|
parentSpace.children.add(newSpace);
|
|
}
|
|
|
|
spaces.add(newSpace);
|
|
_updateNodePosition(newSpace, newSpace.position);
|
|
});
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showEditSpaceDialog(SpaceModel space) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return CreateSpaceDialog(
|
|
products: widget.products,
|
|
name: space.name,
|
|
icon: space.icon,
|
|
isEdit: true,
|
|
selectedProducts: space.selectedProducts,
|
|
onCreateSpace: (String name, String icon,
|
|
List<SelectedProduct> selectedProducts) {
|
|
setState(() {
|
|
// Update the space's properties
|
|
space.name = name;
|
|
space.icon = icon;
|
|
space.selectedProducts = selectedProducts;
|
|
|
|
if (space.status != SpaceStatus.newSpace) {
|
|
space.status = SpaceStatus.modified; // Mark as modified
|
|
}
|
|
});
|
|
},
|
|
key: Key(space.name),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _handleHoverChanged(int index, bool isHovered) {
|
|
setState(() {
|
|
spaces[index].isHovered = isHovered;
|
|
});
|
|
}
|
|
|
|
List<SpaceModel> flattenSpaces(List<SpaceModel> spaces) {
|
|
List<SpaceModel> result = [];
|
|
|
|
void flatten(SpaceModel space) {
|
|
if (space.status == SpaceStatus.deleted) return;
|
|
|
|
result.add(space);
|
|
|
|
for (var child in space.children) {
|
|
flatten(child);
|
|
}
|
|
}
|
|
|
|
for (var space in spaces) {
|
|
flatten(space);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
List<Connection> createConnections(List<SpaceModel> spaces) {
|
|
List<Connection> connections = [];
|
|
|
|
void addConnections(SpaceModel parent, String direction) {
|
|
if (parent.status == SpaceStatus.deleted) return;
|
|
|
|
for (var child in parent.children) {
|
|
if (child.status == SpaceStatus.deleted) continue;
|
|
|
|
connections.add(
|
|
Connection(
|
|
startSpace: parent,
|
|
endSpace: child,
|
|
direction: child.incomingConnection?.direction ?? "down",
|
|
),
|
|
);
|
|
|
|
// Recursively process the child's children
|
|
addConnections(child, direction);
|
|
}
|
|
}
|
|
|
|
for (var space in spaces) {
|
|
addConnections(space, "down");
|
|
}
|
|
|
|
return connections;
|
|
}
|
|
|
|
void _saveSpaces() {
|
|
if (widget.selectedCommunity == null) {
|
|
debugPrint("No community selected for saving spaces.");
|
|
return;
|
|
}
|
|
|
|
List<SpaceModel> spacesToSave = spaces.where((space) {
|
|
return space.status == SpaceStatus.newSpace ||
|
|
space.status == SpaceStatus.modified ||
|
|
space.status == SpaceStatus.deleted;
|
|
}).toList();
|
|
|
|
if (spacesToSave.isEmpty) {
|
|
debugPrint("No new or modified spaces to save.");
|
|
return;
|
|
}
|
|
|
|
String communityUuid = widget.selectedCommunity!.uuid;
|
|
|
|
context.read<SpaceManagementBloc>().add(SaveSpacesEvent(
|
|
spaces: spacesToSave,
|
|
communityUuid: communityUuid,
|
|
));
|
|
}
|
|
|
|
void _onDelete() {
|
|
if (widget.selectedCommunity != null &&
|
|
widget.selectedCommunity?.uuid != null &&
|
|
widget.selectedSpace == null) {
|
|
context.read<SpaceManagementBloc>().add(DeleteCommunityEvent(
|
|
communityUuid: widget.selectedCommunity!.uuid,
|
|
));
|
|
}
|
|
if (widget.selectedSpace != null) {
|
|
setState(() {
|
|
for (var space in spaces) {
|
|
if (space.uuid == widget.selectedSpace?.uuid) {
|
|
space.status = SpaceStatus.deleted;
|
|
_markChildrenAsDeleted(space);
|
|
}
|
|
}
|
|
_removeConnectionsForDeletedSpaces();
|
|
});
|
|
}
|
|
}
|
|
|
|
void _markChildrenAsDeleted(SpaceModel parent) {
|
|
for (var child in parent.children) {
|
|
child.status = SpaceStatus.deleted;
|
|
_markChildrenAsDeleted(child);
|
|
}
|
|
}
|
|
|
|
void _removeConnectionsForDeletedSpaces() {
|
|
connections.removeWhere((connection) {
|
|
return connection.startSpace.status == SpaceStatus.deleted ||
|
|
connection.endSpace.status == SpaceStatus.deleted;
|
|
});
|
|
}
|
|
|
|
void _moveToSpace(SpaceModel space) {
|
|
final double viewportWidth = MediaQuery.of(context).size.width;
|
|
final double viewportHeight = MediaQuery.of(context).size.height;
|
|
|
|
final double dx = -space.position.dx + (viewportWidth / 2) - 400;
|
|
final double dy = -space.position.dy + (viewportHeight / 2) - 300;
|
|
|
|
_transformationController.value = Matrix4.identity()
|
|
..translate(dx, dy)
|
|
..scale(1.2);
|
|
}
|
|
|
|
void _selectSpace(BuildContext context, SpaceModel space) {
|
|
context.read<SpaceManagementBloc>().add(
|
|
SelectSpaceEvent(
|
|
selectedCommunity: widget.selectedCommunity,
|
|
selectedSpace: space),
|
|
);
|
|
}
|
|
|
|
bool _isHighlightedSpace(SpaceModel space) {
|
|
final selectedSpace = widget.selectedSpace;
|
|
if (selectedSpace == null) return true;
|
|
|
|
return space == selectedSpace ||
|
|
selectedSpace.parent?.internalId == space.internalId ||
|
|
selectedSpace.children
|
|
?.any((child) => child.internalId == space.internalId) ==
|
|
true;
|
|
}
|
|
|
|
void _deselectSpace(BuildContext context) {
|
|
context.read<SpaceManagementBloc>().add(
|
|
SelectSpaceEvent(
|
|
selectedCommunity: widget.selectedCommunity, selectedSpace: null),
|
|
);
|
|
}
|
|
|
|
bool _isHighlightedConnection(Connection connection) {
|
|
if (widget.selectedSpace == null) return true;
|
|
|
|
return connection.startSpace == widget.selectedSpace ||
|
|
connection.endSpace == widget.selectedSpace;
|
|
}
|
|
|
|
Offset _getCenterPosition(Size screenSize) {
|
|
return Offset(
|
|
screenSize.width / 2 - 260,
|
|
screenSize.height / 2 - 200,
|
|
);
|
|
}
|
|
|
|
bool isSave(List<SpaceModel> spaces) {
|
|
return spaces.isNotEmpty &&
|
|
spaces.any((space) =>
|
|
space.status == SpaceStatus.newSpace ||
|
|
space.status == SpaceStatus.modified ||
|
|
space.status == SpaceStatus.deleted);
|
|
}
|
|
}
|