added bloc for create community

This commit is contained in:
hannathkadher
2024-12-04 11:05:46 +04:00
parent 0b628c85a5
commit 9bddd151bb
38 changed files with 297 additions and 271 deletions

View File

@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AddDeviceWidget extends StatefulWidget {
final List<ProductModel>? products;
final ValueChanged<List<SelectedProduct>>? onProductsSelected;
final List<SelectedProduct>? initialSelectedProducts;
const AddDeviceWidget({
super.key,
this.products,
this.initialSelectedProducts,
this.onProductsSelected,
});
@override
_AddDeviceWidgetState createState() => _AddDeviceWidgetState();
}
class _AddDeviceWidgetState extends State<AddDeviceWidget> {
late final ScrollController _scrollController;
late List<SelectedProduct> productCounts;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
productCounts =
widget.initialSelectedProducts != null ? List.from(widget.initialSelectedProducts!) : [];
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
// Adjust the GridView properties based on screen width
final crossAxisCount = size.width > 1200
? 8
: size.width > 800
? 5
: 3;
return AlertDialog(
title: const Text('Add Devices'),
backgroundColor: ColorsManager.whiteColors,
content: SingleChildScrollView(
child: Container(
width: size.width * 0.9,
height: size.height * 0.65,
color: ColorsManager.textFieldGreyColor,
child: Column(
children: [
const SizedBox(height: 16),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Scrollbar(
controller: _scrollController,
thumbVisibility: false,
child: GridView.builder(
shrinkWrap: true,
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 6,
crossAxisSpacing: 4,
childAspectRatio: .8,
),
itemCount: widget.products?.length ?? 0,
itemBuilder: (context, index) {
final product = widget.products![index];
return _buildDeviceTypeTile(product, size);
},
),
),
),
),
],
),
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildActionButton('Cancel', ColorsManager.boxColor, ColorsManager.blackColor, () {
Navigator.of(context).pop();
}),
_buildActionButton('Continue', ColorsManager.secondaryColor, Colors.white, () {
Navigator.of(context).pop();
if (widget.onProductsSelected != null) {
widget.onProductsSelected!(productCounts);
}
}),
],
),
],
);
}
Widget _buildDeviceTypeTile(ProductModel product, Size size) {
final selectedProduct = productCounts.firstWhere(
(p) => p.productId == product.uuid,
orElse: () => SelectedProduct(productId: product.uuid, count: 0),
);
return SizedBox(
width: size.width * 0.12,
height: size.height * 0.15,
child: Card(
elevation: 2,
color: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildDeviceIcon(product, size),
const SizedBox(height: 4),
_buildDeviceName(product, size),
const SizedBox(height: 4),
CounterWidget(
initialCount: selectedProduct.count,
onCountChanged: (newCount) {
setState(() {
if (newCount > 0) {
if (!productCounts.contains(selectedProduct)) {
productCounts
.add(SelectedProduct(productId: product.uuid, count: newCount));
} else {
selectedProduct.count = newCount;
}
} else {
productCounts.removeWhere((p) => p.productId == product.uuid);
}
if (widget.onProductsSelected != null) {
widget.onProductsSelected!(productCounts);
}
});
},
),
],
),
),
),
);
}
Widget _buildDeviceIcon(ProductModel product, Size size) {
return Container(
height: size.width > 800 ? 50 : 40,
width: size.width > 800 ? 50 : 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: ColorsManager.textFieldGreyColor,
border: Border.all(
color: ColorsManager.neutralGray,
width: 2,
),
),
child: Center(
child: SvgPicture.asset(
product.icon ?? Assets.sensors,
width: size.width > 800 ? 30 : 20,
height: size.width > 800 ? 30 : 20,
),
),
);
}
Widget _buildDeviceName(ProductModel product, Size size) {
return SizedBox(
height: size.width > 800 ? 35 : 25,
child: Text(
product.name ?? '',
style: context.textTheme.bodySmall?.copyWith(color: ColorsManager.blackColor),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}
Widget _buildActionButton(
String label,
Color backgroundColor,
Color foregroundColor,
VoidCallback onPressed,
) {
return SizedBox(
width: 120,
child: DefaultButton(
onPressed: onPressed,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
child: Text(label),
),
);
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class BlankCommunityWidget extends StatefulWidget {
final List<CommunityModel> communities;
BlankCommunityWidget({required this.communities});
@override
_BlankCommunityWidgetState createState() => _BlankCommunityWidgetState();
}
class _BlankCommunityWidgetState extends State<BlankCommunityWidget> {
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
color:
ColorsManager.whiteColors, // Parent container with white background
child: GridView.builder(
padding: const EdgeInsets.only(left: 40.0, top: 20.0),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 2.0,
),
itemCount: 1, // Only one item
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => _showCreateCommunityDialog(context),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.center, // Center align the content
children: [
Expanded(
child: AspectRatio(
aspectRatio: 2.0,
child: Container(
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 4,
strokeAlign: BorderSide.strokeAlignOutside,
color: ColorsManager.borderColor,
),
borderRadius: BorderRadius.circular(5),
),
),
),
),
),
const SizedBox(height: 9),
Text('Blank',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: ColorsManager.blackColor,
)),
],
));
},
),
),
);
}
void _showCreateCommunityDialog(BuildContext parentContext) {
showDialog(
context: parentContext,
builder: (context) => CreateCommunityDialog(
isEditMode: false,
existingCommunityNames: widget.communities.map((community) => community.name).toList(),
onCreateCommunity: (String communityName, String description) {
parentContext.read<SpaceManagementBloc>().add(
CreateCommunityEvent(
name: communityName,
description: description,
),
);
},
),
);
}
}

View File

@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeader extends StatefulWidget {
final String? communityName;
final bool isEditingName;
final bool isSave;
final TextEditingController nameController;
final VoidCallback onSave;
final VoidCallback onDelete;
final VoidCallback onEditName;
final ValueChanged<String> onNameSubmitted;
final List<CommunityModel> communities;
final CommunityModel? community;
const CommunityStructureHeader(
{super.key,
required this.communityName,
required this.isSave,
required this.isEditingName,
required this.nameController,
required this.onSave,
required this.onDelete,
required this.onEditName,
required this.onNameSubmitted,
this.community,
required this.communities});
@override
State<CommunityStructureHeader> createState() =>
_CommunityStructureHeaderState();
}
class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.shadowBlackColor,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildCommunityInfo(theme, screenWidth),
),
const SizedBox(width: 16),
],
),
],
),
);
}
void _showCreateCommunityDialog(BuildContext parentContext) {
showDialog(
context: parentContext,
builder: (context) => CreateCommunityDialog(
isEditMode: true,
existingCommunityNames:
widget.communities.map((community) => community.name).toList(),
initialName: widget.community?.name ?? '',
onCreateCommunity: (String communityName, String description) {
widget.onNameSubmitted(communityName);
},
),
);
}
Widget _buildCommunityInfo(ThemeData theme, double screenWidth) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Community Structure',
style: theme.textTheme.headlineLarge
?.copyWith(color: ColorsManager.blackColor),
),
if (widget.communityName != null)
Row(
children: [
Expanded(
child: Row(
children: [
if (!widget.isEditingName)
Flexible(
child: Text(
widget.communityName!,
style: theme.textTheme.bodyLarge
?.copyWith(color: ColorsManager.blackColor),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
if (widget.isEditingName)
SizedBox(
width: screenWidth * 0.1,
child: TextField(
controller: widget.nameController,
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
),
style: theme.textTheme.bodyLarge
?.copyWith(color: ColorsManager.blackColor),
onSubmitted: widget.onNameSubmitted,
),
),
const SizedBox(width: 2),
GestureDetector(
onTap: () => _showCreateCommunityDialog(context),
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
],
),
),
if (widget.isSave) ...[
const SizedBox(width: 8),
_buildActionButtons(theme),
],
],
),
],
);
}
Widget _buildActionButtons(ThemeData theme) {
return Wrap(
alignment: WrapAlignment.end,
spacing: 10,
children: [
_buildButton(
label: "Save",
icon: const Icon(Icons.save,
size: 18, color: ColorsManager.spaceColor),
onPressed: widget.onSave,
theme: theme),
],
);
}
Widget _buildButton(
{required String label,
required Widget icon,
required VoidCallback onPressed,
required ThemeData theme}) {
const double buttonHeight = 30;
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: 80, minHeight: buttonHeight),
child: DefaultButton(
onPressed: onPressed,
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: ColorsManager.blackColor,
borderRadius: 8.0,
padding: 2.0,
height: buttonHeight,
elevation: 0,
borderColor: Colors.grey.shade300,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
icon,
const SizedBox(width: 5),
Flexible(
child: Text(
label,
style: theme.textTheme.bodySmall
?.copyWith(color: ColorsManager.blackColor),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,532 @@
// 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/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_community_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/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(
communities: widget.communities,
communityName: widget.selectedCommunity?.name,
community: widget.selectedCommunity,
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,
editSpace: space,
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);
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/custom_expansion_tile.dart';
class CommunityTile extends StatelessWidget {
final String title;
final List<Widget>? children;
final bool isExpanded;
final bool isSelected;
final Function(String, bool) onExpansionChanged;
final Function() onItemSelected;
const CommunityTile({
super.key,
required this.title,
required this.isExpanded,
required this.onExpansionChanged,
required this.onItemSelected,
required this.isSelected,
this.children,
});
@override
Widget build(BuildContext context) {
return CustomExpansionTile(
title: title,
initiallyExpanded: isExpanded,
isSelected: isSelected,
onExpansionChanged: (bool expanded) {
onExpansionChanged(title, expanded);
},
onItemSelected: onItemSelected,
children: children ?? [],
);
}
}

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CounterWidget extends StatefulWidget {
final int initialCount;
final ValueChanged<int> onCountChanged;
const CounterWidget({
Key? key,
this.initialCount = 0,
required this.onCountChanged,
}) : super(key: key);
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
late int _counter;
@override
void initState() {
super.initState();
_counter = widget.initialCount;
}
void _incrementCounter() {
setState(() {
_counter++;
widget.onCountChanged(_counter);
});
}
void _decrementCounter() {
setState(() {
if (_counter > 0) {
_counter--;
widget.onCountChanged(_counter);
}
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: ColorsManager.counterBackgroundColor,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildCounterButton(Icons.remove, _decrementCounter),
const SizedBox(width: 8),
Text(
'$_counter',
style: theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.spaceColor),
),
const SizedBox(width: 8),
_buildCounterButton(Icons.add, _incrementCounter),
],
),
);
}
Widget _buildCounterButton(IconData icon, VoidCallback onPressed) {
return GestureDetector(
onTap: onPressed,
child: Icon(
icon,
color: ColorsManager.spaceColor,
size: 18,
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.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.endSpace.position == null) {
continue;
}
Offset start = connection.startSpace.position +
const Offset(75, 60); // Center bottom of start space
Offset end = connection.endSpace.position +
const 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') {
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') {
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);
}
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,408 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/constants/space_icon_const.dart';
class CreateSpaceDialog extends StatefulWidget {
final Function(String, String, List<SelectedProduct> selectedProducts)
onCreateSpace;
final List<ProductModel>? products;
final String? name;
final String? icon;
final bool isEdit;
final List<SelectedProduct> selectedProducts;
final SpaceModel? parentSpace;
final SpaceModel? editSpace;
const CreateSpaceDialog(
{super.key,
this.parentSpace,
required this.onCreateSpace,
this.products,
this.name,
this.icon,
this.isEdit = false,
this.editSpace,
this.selectedProducts = const []});
@override
CreateSpaceDialogState createState() => CreateSpaceDialogState();
}
class CreateSpaceDialogState extends State<CreateSpaceDialog> {
String selectedIcon = Assets.location;
String enteredName = '';
List<SelectedProduct> selectedProducts = [];
late TextEditingController nameController;
bool isOkButtonEnabled = false;
bool isNameFieldInvalid = false;
bool isNameFieldExist = false;
@override
void initState() {
super.initState();
selectedIcon = widget.icon ?? Assets.location;
nameController = TextEditingController(text: widget.name ?? '');
selectedProducts =
widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled =
enteredName.isNotEmpty || nameController.text.isNotEmpty;
}
@override
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return AlertDialog(
title: widget.isEdit
? const Text('Edit Space')
: const Text('Create New Space'),
backgroundColor: ColorsManager.whiteColors,
content: SizedBox(
width: screenWidth * 0.5, // Limit dialog width
child: SingleChildScrollView(
// Scrollable content to prevent overflow
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
alignment: Alignment.center,
children: [
Container(
width: screenWidth * 0.1, // Adjusted width
height: screenWidth * 0.1, // Adjusted height
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
),
SvgPicture.asset(
selectedIcon,
width: screenWidth * 0.04,
height: screenWidth * 0.04,
),
Positioned(
top: 6,
right: 6,
child: InkWell(
onTap: _showIconSelectionDialog,
child: Container(
width: screenWidth * 0.020,
height: screenWidth * 0.020,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: SvgPicture.asset(
Assets.iconEdit,
width: screenWidth * 0.06,
height: screenWidth * 0.06,
),
),
),
),
],
),
const SizedBox(width: 16),
Expanded(
// Ensure the text field expands responsively
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: nameController,
onChanged: (value) {
enteredName = value.trim();
setState(() {
isNameFieldExist = false;
isOkButtonEnabled = false;
isNameFieldInvalid = value.isEmpty;
if (!isNameFieldInvalid) {
if ((widget.parentSpace?.children.any(
(child) => child.name == value) ??
false) ||
(widget.parentSpace?.name == value) ||
(widget.editSpace?.children.any(
(child) => child.name == value) ??
false)) {
isNameFieldExist = true;
isOkButtonEnabled = false;
} else {
isNameFieldExist = false;
isOkButtonEnabled = true;
}
}
});
},
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: 'Please enter the name',
hintStyle: const TextStyle(
fontSize: 14,
color: ColorsManager.lightGrayColor,
fontWeight: FontWeight.w400,
),
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: isNameFieldInvalid || isNameFieldExist
? ColorsManager.red
: ColorsManager.boxColor,
width: 1.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: ColorsManager.boxColor,
width: 1.5,
),
),
),
),
if (isNameFieldInvalid)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Space name should not be empty.',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
if (isNameFieldExist)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Name already exist',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
const SizedBox(height: 16),
if (selectedProducts.isNotEmpty)
_buildSelectedProductsButtons(widget.products ?? [])
else
DefaultButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AddDeviceWidget(
products: widget.products,
onProductsSelected: (selectedProductsMap) {
setState(() {
selectedProducts = selectedProductsMap;
});
},
),
);
},
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: Colors.black,
borderColor: ColorsManager.neutralGray,
borderRadius: 16.0,
padding: 10.0, // Reduced padding for smaller size
child: Align(
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 6.0),
child: SvgPicture.asset(
Assets.addIcon,
width: screenWidth *
0.015, // Adjust icon size
height: screenWidth * 0.015,
),
),
const SizedBox(width: 3),
Flexible(
child: Text(
'Add devices / Assign a space model',
overflow: TextOverflow
.ellipsis, // Prevent overflow
style: Theme.of(context)
.textTheme
.bodyMedium,
),
),
],
),
)),
],
),
),
],
),
],
),
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () => Navigator.of(context).pop(),
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
onPressed: () {
if (nameController.text.isEmpty) {
setState(() {
isNameFieldInvalid = true;
});
return;
} else {
String newName = enteredName.isNotEmpty
? enteredName
: (widget.name ?? '');
if (newName.isNotEmpty) {
widget.onCreateSpace(
newName, selectedIcon, selectedProducts);
Navigator.of(context).pop();
}
}
},
borderRadius: 10,
backgroundColor: isOkButtonEnabled
? ColorsManager.secondaryColor
: ColorsManager.grayColor,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
),
],
),
],
);
}
void _showIconSelectionDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return IconSelectionDialog(
spaceIconList: spaceIconList,
onIconSelected: (String selectedIcon) {
setState(() {
this.selectedIcon = selectedIcon;
});
},
);
},
);
}
Widget _buildSelectedProductsButtons(List<ProductModel> products) {
final screenWidth = MediaQuery.of(context).size.width;
return Container(
width: screenWidth * 0.6,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: ColorsManager.neutralGray,
width: 2,
),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0; i < selectedProducts.length; i++) ...[
HoverableButton(
iconPath:
_mapIconToProduct(selectedProducts[i].productId, products),
text: 'x${selectedProducts[i].count}',
onTap: () {
setState(() {
selectedProducts.remove(selectedProducts[i]);
});
// Handle button tap
},
),
if (i < selectedProducts.length - 1)
const SizedBox(
width: 2), // Add space except after the last button
],
const SizedBox(width: 2),
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => AddDeviceWidget(
products: widget.products,
initialSelectedProducts: selectedProducts,
onProductsSelected: (selectedProductsMap) {
setState(() {
selectedProducts = selectedProductsMap;
});
},
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.add,
color: ColorsManager.spaceColor,
size: 24,
),
),
),
],
),
);
}
String _mapIconToProduct(String uuid, List<ProductModel> products) {
// Find the product with the matching UUID
final product = products.firstWhere(
(product) => product.uuid == uuid,
orElse: () => ProductModel(
uuid: '',
catName: '',
prodId: '',
prodType: '',
name: '',
icon: Assets.presenceSensor,
),
);
return product.icon ?? Assets.presenceSensor;
}
}

View File

@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
void showDeleteConfirmationDialog(BuildContext context, VoidCallback onConfirm, bool isSpace) {
final String title = isSpace ? 'Delete Space' : 'Delete Community';
final String subtitle = isSpace
? 'All the data in the space will be lost'
: 'All the data in the community will be lost';
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
child: SizedBox(
width: 500,
child: Container(
color: ColorsManager.whiteColors,
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildWarningIcon(),
const SizedBox(height: 20),
_buildDialogTitle(context, title),
const SizedBox(height: 10),
_buildDialogSubtitle(context, subtitle),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CancelButton(
label: 'Cancel',
onPressed: () => Navigator.of(context).pop(),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(); // Close the first dialog
showProcessingPopup(context, isSpace, onConfirm);
},
style: _dialogButtonStyle(Colors.blue),
child: const Text('Continue', style: TextStyle(color: Colors.white)),
),
],
),
],
),
),
),
);
},
);
}
void showProcessingPopup(BuildContext context, bool isSpace, VoidCallback onDelete) {
final String title = isSpace ? 'Delete Space' : 'Delete Community';
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
child: SizedBox(
width: 500,
child: Container(
color: ColorsManager.whiteColors,
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildWarningIcon(),
const SizedBox(height: 20),
_buildDialogTitle(context, title),
const SizedBox(height: 10),
_buildDialogSubtitle(context, 'Are you sure you want to delete?'),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: onDelete,
style: _dialogButtonStyle(ColorsManager.warningRed),
child: const Text('Delete', style: TextStyle(color: Colors.white)),
),
CancelButton(
label: 'Cancel',
onPressed: () => Navigator.of(context).pop(),
),
],
),
],
),
),
),
);
},
);
}
Widget _buildWarningIcon() {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: ColorsManager.warningRed,
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white, size: 40),
);
}
Widget _buildDialogTitle(BuildContext context, String title) {
return Text(
title,
style: Theme.of(context).textTheme.headlineMedium,
);
}
Widget _buildDialogSubtitle(BuildContext context, String subtitle) {
return Text(
subtitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: ColorsManager.grayColor),
);
}
ButtonStyle _dialogButtonStyle(Color color) {
return ElevatedButton.styleFrom(
backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
fixedSize: const Size(140, 40),
);
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class IconSelectionDialog extends StatelessWidget {
final List<String> spaceIconList;
final Function(String selectedIcon) onIconSelected;
const IconSelectionDialog({
Key? key,
required this.spaceIconList,
required this.onIconSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
return Dialog(
elevation: 0,
backgroundColor: ColorsManager.transparentColor,
child: Container(
width: screenWidth * 0.44,
height: screenHeight * 0.45,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2), // Shadow color
blurRadius: 20, // Spread of the blur
offset: const Offset(0, 8), // Offset of the shadow
),
],
),
child: AlertDialog(
title: Text('Space Icon',style: Theme.of(context).textTheme.headlineMedium),
backgroundColor: ColorsManager.whiteColors,
content: Container(
width: screenWidth * 0.4,
height: screenHeight * 0.45,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(12),
),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
crossAxisSpacing: 8,
mainAxisSpacing: 16,
),
itemCount: spaceIconList.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
onIconSelected(spaceIconList[index]);
Navigator.of(context).pop();
},
child: SvgPicture.asset(
spaceIconList[index],
width: screenWidth * 0.03,
height: screenWidth * 0.03,
),
);
},
),
),
),
),
);
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class GradientCanvasBorderWidget extends StatelessWidget {
final double top;
final double bottom;
final double left;
final double width;
const GradientCanvasBorderWidget({
super.key,
this.top = 0,
this.bottom = 0,
this.left = 300,
this.width = 8,
});
@override
Widget build(BuildContext context) {
return Positioned(
top: top,
bottom: bottom,
left: left,
width: width,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
ColorsManager.semiTransparentBlackColor.withOpacity(0.1),
ColorsManager.transparentColor,
],
),
),
),
);
}
}

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class HoverableButton extends StatefulWidget {
final String iconPath;
final String text;
final VoidCallback onTap;
const HoverableButton({
Key? key,
required this.iconPath,
required this.text,
required this.onTap,
}) : super(key: key);
@override
State<HoverableButton> createState() => _HoverableButtonState();
}
class _HoverableButtonState extends State<HoverableButton> {
bool isHovered = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
return GestureDetector(
onTap: widget.onTap,
child: MouseRegion(
onEnter: (_) => _updateHoverState(true),
onExit: (_) => _updateHoverState(false),
child: SizedBox(
width: screenWidth * .07,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 8),
decoration: BoxDecoration(
color: isHovered ? ColorsManager.warningRed : ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(16),
boxShadow: [
if (isHovered)
BoxShadow(
color: ColorsManager.warningRed.withOpacity(0.4),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildIcon(),
if (!isHovered) const SizedBox(width: 8),
if (!isHovered) _buildText(theme),
],
),
)),
)),
);
}
Widget _buildIcon() {
return isHovered
? const Icon(
Icons.close,
color: ColorsManager.whiteColors,
size: 24,
)
: SvgPicture.asset(
widget.iconPath,
width: 24,
height: 24,
);
}
Widget _buildText(ThemeData theme) {
return Text(
widget.text,
style: theme.textTheme.bodyLarge?.copyWith(
color: ColorsManager.spaceColor,
fontWeight: FontWeight.w500,
),
);
}
void _updateHoverState(bool hover) {
setState(() => isHovered = hover);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/gradient_canvas_border_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart';
class LoadedSpaceView extends StatefulWidget {
final List<CommunityModel> communities;
final CommunityModel? selectedCommunity;
final SpaceModel? selectedSpace;
final List<ProductModel>? products;
const LoadedSpaceView({
super.key,
required this.communities,
this.selectedCommunity,
this.selectedSpace,
this.products,
});
@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,
selectedSpaceUuid: widget.selectedSpace?.uuid ??
widget.selectedCommunity?.uuid ??
'',
),
CommunityStructureArea(
selectedCommunity: widget.selectedCommunity,
selectedSpace: widget.selectedSpace,
spaces: widget.selectedCommunity?.spaces ?? [],
products: widget.products,
communities: widget.communities,
),
],
),
const GradientCanvasBorderWidget(),
],
);
}
SpaceModel? findSpaceByUuid(String? uuid, List<CommunityModel> communities) {
for (var community in communities) {
for (var space in community.spaces) {
if (space.uuid == uuid) return space;
}
}
return null;
}
}

View File

@ -0,0 +1,53 @@
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 Function(int index, Offset newPosition, String direction) onButtonTap;
const PlusButtonWidget({
super.key,
required this.index,
required this.direction,
required this.offset,
required this.onButtonTap,
});
@override
Widget build(BuildContext context) {
return Positioned(
left: offset.dx,
top: offset.dy,
child: GestureDetector(
onTap: () {
Offset newPosition;
switch (direction) {
case 'left':
newPosition = const Offset(-200, 0);
break;
case 'right':
newPosition = const Offset(200, 0);
break;
case 'down':
newPosition = const Offset(0, 150);
break;
default:
newPosition = Offset.zero;
}
onButtonTap(index, newPosition, direction);
},
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(Icons.add, color: Colors.white, size: 20),
),
),
);
}
}

View File

@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/common/search_bar.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class SidebarWidget extends StatefulWidget {
final List<CommunityModel> communities;
final String? selectedSpaceUuid;
const SidebarWidget({
super.key,
required this.communities,
this.selectedSpaceUuid,
});
@override
_SidebarWidgetState createState() => _SidebarWidgetState();
}
class _SidebarWidgetState extends State<SidebarWidget> {
String _searchQuery = ''; // Track search query
String? _selectedSpaceUuid;
String? _selectedId;
@override
void initState() {
super.initState();
_selectedId = widget
.selectedSpaceUuid; // Initialize with the passed selected space UUID
}
@override
void didUpdateWidget(covariant SidebarWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) {
setState(() {
_selectedId = widget.selectedSpaceUuid;
});
}
}
// Function to filter communities based on the search query
List<CommunityModel> _filterCommunities() {
if (_searchQuery.isEmpty) {
// Reset the selected community and space UUIDs if there's no query
_selectedSpaceUuid = null;
return widget.communities;
}
// Filter communities and expand only those that match the query
return widget.communities.where((community) {
final containsQueryInCommunity =
community.name.toLowerCase().contains(_searchQuery.toLowerCase());
final containsQueryInSpaces = community.spaces
.any((space) => _containsQuery(space, _searchQuery.toLowerCase()));
return containsQueryInCommunity || containsQueryInSpaces;
}).toList();
}
// Helper function to determine if any space or its children match the search query
bool _containsQuery(SpaceModel space, String query) {
final matchesSpace = space.name.toLowerCase().contains(query);
final matchesChildren = space.children.any((child) =>
_containsQuery(child, query)); // Recursive check for children
// If the space or any of its children match the query, expand this space
if (matchesSpace || matchesChildren) {
_selectedSpaceUuid = space.uuid;
}
return matchesSpace || matchesChildren;
}
bool _isSpaceOrChildSelected(SpaceModel space) {
// Return true if the current space or any of its child spaces is selected
if (_selectedSpaceUuid == space.uuid) {
return true;
}
// Recursively check if any child spaces match the query
for (var child in space.children) {
if (_isSpaceOrChildSelected(child)) {
return true;
}
}
return false;
}
@override
Widget build(BuildContext context) {
final filteredCommunities = _filterCommunities();
return Container(
width: 300,
decoration: subSectionContainerDecoration,
child: Column(
mainAxisSize:
MainAxisSize.min, // Ensures the Column only takes necessary height
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Communities title with the add button
Container(
decoration: subSectionContainerDecoration,
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Communities',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.black,
)),
GestureDetector(
onTap: () => _navigateToBlank(context),
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
shape: BoxShape.circle,
),
child: Center(
child: SvgPicture.asset(
Assets.roundedAddIcon,
width: 24,
height: 24,
),
),
),
),
],
),
),
// Search bar
CustomSearchBar(
onSearchChanged: (query) {
setState(() {
_searchQuery = query;
});
},
),
const SizedBox(height: 16),
// Community list
Expanded(
child: ListView(
children: filteredCommunities.map((community) {
return _buildCommunityTile(context, community);
}).toList(),
),
),
],
),
);
}
void _navigateToBlank(BuildContext context) {
setState(() {
_selectedId = '';
});
context.read<SpaceManagementBloc>().add(
NewCommunityEvent(communities: widget.communities),
);
}
Widget _buildCommunityTile(BuildContext context, CommunityModel community) {
bool hasChildren = community.spaces.isNotEmpty;
return CommunityTile(
title: community.name,
key: ValueKey(community.uuid),
isSelected: _selectedId == community.uuid,
isExpanded: false,
onItemSelected: () {
setState(() {
_selectedId = community.uuid;
_selectedSpaceUuid = null; // Update the selected community
});
context.read<SpaceManagementBloc>().add(
SelectCommunityEvent(selectedCommunity: community),
);
},
onExpansionChanged: (String title, bool expanded) {
_handleExpansionChange(community.uuid, expanded);
},
children: hasChildren
? community.spaces
.map((space) => _buildSpaceTile(space, community))
.toList()
: null, // Render spaces within the community
);
}
Widget _buildSpaceTile(SpaceModel space, CommunityModel community) {
bool isExpandedSpace = _isSpaceOrChildSelected(space);
return SpaceTile(
title: space.name,
key: ValueKey(space.uuid),
isSelected: _selectedId == space.uuid,
initiallyExpanded: isExpandedSpace,
onExpansionChanged: (bool expanded) {
_handleExpansionChange(space.uuid ?? '', expanded);
},
onItemSelected: () {
setState(() {
_selectedId = space.uuid;
_selectedSpaceUuid = space.uuid;
});
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(
selectedCommunity: community, selectedSpace: space),
);
},
children: space.children.isNotEmpty
? space.children
.map((childSpace) => _buildSpaceTile(childSpace, community))
.toList()
: [], // Recursively render child spaces if available
);
}
void _handleExpansionChange(String uuid, bool expanded) {}
}

View File

@ -0,0 +1,74 @@
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, 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.onHoverChanged,
required this.onButtonTap,
required this.buildSpaceContainer,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
// Call the provided callback to update the position
final newPosition = position + details.delta;
onPositionChanged(newPosition);
},
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, // Allow hovering elements to be displayed outside the boundary
children: [
buildSpaceContainer(index), // Build the space container
if (isHovered) ...[
PlusButtonWidget(
index: index,
direction: 'left',
offset: const Offset(-21, 20),
onButtonTap: onButtonTap,
),
PlusButtonWidget(
index: index,
direction: 'right',
offset: const Offset(140, 20),
onButtonTap: onButtonTap,
),
PlusButtonWidget(
index: index,
direction: 'down',
offset: const Offset(63, 50),
onButtonTap: onButtonTap,
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,89 @@
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;
final VoidCallback? onDoubleTap;
final VoidCallback? onTap;
const SpaceContainerWidget({
super.key,
required this.index,
required this.icon,
required this.name,
this.onTap,
this.onDoubleTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
onDoubleTap: onDoubleTap,
onTap: onTap,
child: Container(
width: 150,
height: 60,
decoration: _containerDecoration(),
child: Row(
children: [
_buildIconContainer(),
const SizedBox(width: 10),
Expanded(
child: Text(
name,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.blackColor,
),
overflow: TextOverflow.ellipsis, // Handle long names gracefully
),
),
],
),
),
);
}
/// Builds the icon container with the SVG asset.
Widget _buildIconContainer() {
return Container(
width: 40,
height: double.infinity,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
bottomLeft: Radius.circular(15),
),
),
child: Center(
child: SvgPicture.asset(
icon,
color: ColorsManager.whiteColors,
width: 24,
height: 24,
),
),
);
}
BoxDecoration _containerDecoration() {
return 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
),
],
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/custom_expansion_tile.dart';
class SpaceTile extends StatefulWidget {
final String title;
final bool isSelected;
final bool initiallyExpanded;
final ValueChanged<bool> onExpansionChanged;
final List<Widget>? children;
final Function() onItemSelected;
const SpaceTile({
super.key,
required this.title,
required this.initiallyExpanded,
required this.onExpansionChanged,
required this.onItemSelected,
required this.isSelected,
this.children,
});
@override
_SpaceTileState createState() => _SpaceTileState();
}
class _SpaceTileState extends State<SpaceTile> {
late bool _isExpanded;
@override
void initState() {
super.initState();
_isExpanded = widget.initiallyExpanded;
}
@override
Widget build(BuildContext context) {
return CustomExpansionTile(
isSelected: widget.isSelected,
title: widget.title,
initiallyExpanded: _isExpanded,
onItemSelected: widget.onItemSelected,
onExpansionChanged: (bool expanded) {
setState(() {
_isExpanded = expanded;
});
widget.onExpansionChanged(expanded);
},
children: widget.children ?? [],
);
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
class SpaceWidget extends StatelessWidget {
final String name;
final Offset position;
final VoidCallback onTap;
const SpaceWidget({
super.key,
required this.name,
required this.position,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: GestureDetector(
onTap: onTap,
child:
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
const Icon(Icons.location_on, color: Colors.blue),
const SizedBox(width: 8),
Text(name, style: const TextStyle(fontSize: 16)),
],
),
),
),
);
}
}