mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 07:07:19 +00:00
Sp 1713 implement empty state (#285)
<!-- Thanks for contributing! Provide a description of your changes below and a general summary in the title Please look at the following checklist to ensure that your PR can be accepted quickly: --> ## Jira Ticket [SP-1713](https://syncrow.atlassian.net/browse/SP-1713) ## Description Implemented non selected space state Implemented an initial version of the canvas. ## Type of Change <!--- Put an `x` in all the boxes that apply: --> - [x] ✨ New feature (non-breaking change which adds functionality) - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) - [ ] 🧹 Code refactor - [ ] ✅ Build configuration change - [ ] 📝 Documentation - [ ] 🗑️ Chore [SP-1713]: https://syncrow.atlassian.net/browse/SP-1713?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
This commit is contained in:
@ -0,0 +1,6 @@
|
|||||||
|
class SpaceConnectionModel {
|
||||||
|
final String from;
|
||||||
|
final String to;
|
||||||
|
|
||||||
|
const SpaceConnectionModel({required this.from, required this.to});
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||||
|
final List<SpaceConnectionModel> connections;
|
||||||
|
final Map<String, Offset> positions;
|
||||||
|
final double cardWidth = 150.0;
|
||||||
|
final double cardHeight = 90.0;
|
||||||
|
final String? selectedSpaceUuid;
|
||||||
|
|
||||||
|
SpacesConnectionsArrowPainter({
|
||||||
|
required this.connections,
|
||||||
|
required this.positions,
|
||||||
|
this.selectedSpaceUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
for (final connection in connections) {
|
||||||
|
final isSelected = connection.to == selectedSpaceUuid;
|
||||||
|
final paint = Paint()
|
||||||
|
..color = isSelected
|
||||||
|
? ColorsManager.primaryColor
|
||||||
|
: ColorsManager.blackColor.withValues(alpha: 0.5)
|
||||||
|
..strokeWidth = 2.0
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
final from = positions[connection.from];
|
||||||
|
final to = positions[connection.to];
|
||||||
|
|
||||||
|
if (from != null && to != null) {
|
||||||
|
final startPoint =
|
||||||
|
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10);
|
||||||
|
final endPoint = Offset(to.dx + cardWidth / 2, to.dy);
|
||||||
|
|
||||||
|
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
|
||||||
|
|
||||||
|
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60);
|
||||||
|
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
|
||||||
|
|
||||||
|
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
|
||||||
|
controlPoint2.dy, endPoint.dx, endPoint.dy);
|
||||||
|
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
|
||||||
|
final circlePaint = Paint()
|
||||||
|
..color = isSelected
|
||||||
|
? ColorsManager.primaryColor
|
||||||
|
: ColorsManager.blackColor.withValues(alpha: 0.5)
|
||||||
|
..style = PaintingStyle.fill
|
||||||
|
..blendMode = BlendMode.srcIn;
|
||||||
|
canvas.drawCircle(endPoint, 4, circlePaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
|
||||||
|
|
||||||
|
abstract final class SpaceManagementCommunityDialogHelper {
|
||||||
|
static void showCreateDialog(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => CreateCommunityDialog(
|
||||||
|
title: const SelectableText('Community Name'),
|
||||||
|
onCreateCommunity: (community) {
|
||||||
|
context.read<CommunitiesBloc>().add(
|
||||||
|
InsertCommunity(community),
|
||||||
|
);
|
||||||
|
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||||
|
SelectCommunityEvent(community: community),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,236 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
|
||||||
|
class CommunityStructureCanvas extends StatefulWidget {
|
||||||
|
const CommunityStructureCanvas({
|
||||||
|
required this.community,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CommunityModel community;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CommunityStructureCanvas> createState() =>_CommunityStructureCanvasState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
final Map<String, Offset> _positions = {};
|
||||||
|
final double _cardWidth = 150.0;
|
||||||
|
final double _cardHeight = 90.0;
|
||||||
|
final double _horizontalSpacing = 150.0;
|
||||||
|
final double _verticalSpacing = 120.0;
|
||||||
|
String? _selectedSpaceUuid;
|
||||||
|
|
||||||
|
late TransformationController _transformationController;
|
||||||
|
late AnimationController _animationController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_transformationController = TransformationController();
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_transformationController.dispose();
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _runAnimation(Matrix4 target) {
|
||||||
|
final animation = Matrix4Tween(
|
||||||
|
begin: _transformationController.value,
|
||||||
|
end: target,
|
||||||
|
).animate(_animationController);
|
||||||
|
|
||||||
|
void listener() {
|
||||||
|
_transformationController.value = animation.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
animation.addListener(listener);
|
||||||
|
_animationController.forward(from: 0).whenCompleteOrCancel(() {
|
||||||
|
animation.removeListener(listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSpaceTapped(String spaceUuid) {
|
||||||
|
setState(() {
|
||||||
|
_selectedSpaceUuid = spaceUuid;
|
||||||
|
});
|
||||||
|
|
||||||
|
final position = _positions[spaceUuid];
|
||||||
|
if (position == null) return;
|
||||||
|
|
||||||
|
const scale = 2.0;
|
||||||
|
final viewSize = context.size;
|
||||||
|
if (viewSize == null) return;
|
||||||
|
|
||||||
|
final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2);
|
||||||
|
final y =
|
||||||
|
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
|
||||||
|
|
||||||
|
final matrix = Matrix4.identity()
|
||||||
|
..translate(x, y)
|
||||||
|
..scale(scale);
|
||||||
|
|
||||||
|
_runAnimation(matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetSelectionAndZoom() {
|
||||||
|
setState(() {
|
||||||
|
_selectedSpaceUuid = null;
|
||||||
|
});
|
||||||
|
_runAnimation(Matrix4.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateLayout(
|
||||||
|
List<SpaceModel> spaces,
|
||||||
|
int depth,
|
||||||
|
Map<int, double> levelXOffset,
|
||||||
|
) {
|
||||||
|
for (final space in spaces) {
|
||||||
|
double childSubtreeWidth = 0;
|
||||||
|
if (space.children.isNotEmpty) {
|
||||||
|
_calculateLayout(space.children, depth + 1, levelXOffset);
|
||||||
|
final firstChildPos = _positions[space.children.first.uuid];
|
||||||
|
final lastChildPos = _positions[space.children.last.uuid];
|
||||||
|
if (firstChildPos != null && lastChildPos != null) {
|
||||||
|
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentX = levelXOffset.putIfAbsent(depth, () => 0.0);
|
||||||
|
double? x;
|
||||||
|
|
||||||
|
if (space.children.isNotEmpty) {
|
||||||
|
final firstChildPos = _positions[space.children.first.uuid]!;
|
||||||
|
x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2;
|
||||||
|
} else {
|
||||||
|
x = currentX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < currentX) {
|
||||||
|
final shiftX = currentX - x;
|
||||||
|
_shiftSubtree(space, shiftX);
|
||||||
|
final keysToShift = levelXOffset.keys.where((d) => d > depth).toList();
|
||||||
|
for (final key in keysToShift) {
|
||||||
|
levelXOffset[key] = levelXOffset[key]! + shiftX;
|
||||||
|
}
|
||||||
|
x += shiftX;
|
||||||
|
}
|
||||||
|
|
||||||
|
final y = depth * (_verticalSpacing + _cardHeight);
|
||||||
|
_positions[space.uuid] = Offset(x, y);
|
||||||
|
levelXOffset[depth] = x + _cardWidth + _horizontalSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _shiftSubtree(SpaceModel space, double shiftX) {
|
||||||
|
if (_positions.containsKey(space.uuid)) {
|
||||||
|
_positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0);
|
||||||
|
}
|
||||||
|
for (final child in space.children) {
|
||||||
|
_shiftSubtree(child, shiftX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildTreeWidgets() {
|
||||||
|
_positions.clear();
|
||||||
|
final community = widget.community;
|
||||||
|
|
||||||
|
_calculateLayout(community.spaces, 0, {});
|
||||||
|
|
||||||
|
final widgets = <Widget>[];
|
||||||
|
final connections = <SpaceConnectionModel>[];
|
||||||
|
_generateWidgets(community.spaces, widgets, connections);
|
||||||
|
|
||||||
|
return [
|
||||||
|
CustomPaint(
|
||||||
|
painter: SpacesConnectionsArrowPainter(
|
||||||
|
connections: connections,
|
||||||
|
positions: _positions,
|
||||||
|
selectedSpaceUuid: _selectedSpaceUuid,
|
||||||
|
),
|
||||||
|
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _generateWidgets(
|
||||||
|
List<SpaceModel> spaces,
|
||||||
|
List<Widget> widgets,
|
||||||
|
List<SpaceConnectionModel> connections,
|
||||||
|
) {
|
||||||
|
for (final space in spaces) {
|
||||||
|
final position = _positions[space.uuid];
|
||||||
|
if (position == null) continue;
|
||||||
|
|
||||||
|
widgets.add(
|
||||||
|
Positioned(
|
||||||
|
left: position.dx,
|
||||||
|
top: position.dy,
|
||||||
|
width: _cardWidth,
|
||||||
|
height: _cardHeight,
|
||||||
|
child: SpaceCardWidget(
|
||||||
|
index: spaces.indexOf(space),
|
||||||
|
onPositionChanged: (newPosition) {},
|
||||||
|
buildSpaceContainer: (index) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: 1.0,
|
||||||
|
child: SpaceCell(
|
||||||
|
index: index,
|
||||||
|
onTap: () => _onSpaceTapped(space.uuid),
|
||||||
|
icon: space.icon,
|
||||||
|
name: space.spaceName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
screenSize: MediaQuery.sizeOf(context),
|
||||||
|
position: position,
|
||||||
|
isHovered: false,
|
||||||
|
onHoverChanged: (int index, bool isHovered) {},
|
||||||
|
onButtonTap: (int index, Offset newPosition) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final child in space.children) {
|
||||||
|
connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
|
||||||
|
}
|
||||||
|
_generateWidgets(space.children, widgets, connections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final treeWidgets = _buildTreeWidgets();
|
||||||
|
return InteractiveViewer(
|
||||||
|
transformationController: _transformationController,
|
||||||
|
boundaryMargin: EdgeInsets.symmetric(
|
||||||
|
horizontal: MediaQuery.sizeOf(context).width * 0.3,
|
||||||
|
vertical: MediaQuery.sizeOf(context).height * 0.2,
|
||||||
|
),
|
||||||
|
minScale: 0.5,
|
||||||
|
maxScale: 3.0,
|
||||||
|
constrained: false,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _resetSelectionAndZoom,
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.sizeOf(context).width * 2,
|
||||||
|
height: MediaQuery.sizeOf(context).height * 2,
|
||||||
|
child: Stack(children: treeWidgets),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class CommunityTemplateCell extends StatelessWidget {
|
||||||
|
const CommunityTemplateCell({
|
||||||
|
super.key,
|
||||||
|
required this.onTap,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function() onTap;
|
||||||
|
final Widget title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Column(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DefaultTextStyle(
|
||||||
|
style: context.textTheme.bodyLarge!.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
child: title,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class CreateSpaceButton extends StatelessWidget {
|
||||||
|
const CreateSpaceButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: Container(
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withValues(alpha: 0.5),
|
||||||
|
spreadRadius: 5,
|
||||||
|
blurRadius: 7,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: ColorsManager.boxColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
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 void Function(int index, Offset newPosition) onButtonTap;
|
||||||
|
|
||||||
|
const PlusButtonWidget({
|
||||||
|
super.key,
|
||||||
|
required this.index,
|
||||||
|
required this.direction,
|
||||||
|
required this.offset,
|
||||||
|
required this.onButtonTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (direction == 'down') {
|
||||||
|
onButtonTap(index, const Offset(0, 150));
|
||||||
|
} else {
|
||||||
|
onButtonTap(index, const Offset(150, 0));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: ColorsManager.spaceColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart';
|
||||||
|
|
||||||
|
class SpaceCardWidget extends StatelessWidget {
|
||||||
|
final int index;
|
||||||
|
final Size screenSize;
|
||||||
|
final Offset position;
|
||||||
|
final bool isHovered;
|
||||||
|
final void Function(int index, bool isHovered) onHoverChanged;
|
||||||
|
final void Function(int index, Offset newPosition) 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 MouseRegion(
|
||||||
|
onEnter: (_) => onHoverChanged(index, true),
|
||||||
|
onExit: (_) => onHoverChanged(index, false),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 150,
|
||||||
|
height: 90,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
buildSpaceContainer(index),
|
||||||
|
|
||||||
|
if (isHovered)
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
child: PlusButtonWidget(
|
||||||
|
index: index,
|
||||||
|
direction: 'down',
|
||||||
|
offset: Offset.zero,
|
||||||
|
onButtonTap: onButtonTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isHovered)
|
||||||
|
Positioned(
|
||||||
|
right: -15,
|
||||||
|
child: PlusButtonWidget(
|
||||||
|
index: index,
|
||||||
|
direction: 'right',
|
||||||
|
offset: Offset.zero,
|
||||||
|
onButtonTap: onButtonTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class SpaceCell extends StatelessWidget {
|
||||||
|
final int index;
|
||||||
|
final String icon;
|
||||||
|
final String name;
|
||||||
|
final VoidCallback? onDoubleTap;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const SpaceCell({
|
||||||
|
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: 70,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: ColorsManager.lightGrayColor.withValues(alpha: 0.5),
|
||||||
|
spreadRadius: 2,
|
||||||
|
blurRadius: 5,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart';
|
||||||
|
|
||||||
class SpaceManagementBody extends StatelessWidget {
|
class SpaceManagementBody extends StatelessWidget {
|
||||||
@ -6,9 +10,21 @@ class SpaceManagementBody extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
SpaceManagementCommunitiesTree(),
|
const SpaceManagementCommunitiesTree(),
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<CommunitiesTreeSelectionBloc,
|
||||||
|
CommunitiesTreeSelectionState>(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.selectedCommunity != current.selectedCommunity,
|
||||||
|
builder: (context, state) => Visibility(
|
||||||
|
visible: state.selectedCommunity == null,
|
||||||
|
replacement: const SpaceManagementCommunityStructure(),
|
||||||
|
child: const SpaceManagementTemplatesView(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
|
||||||
|
class SpaceManagementCommunityStructure extends StatelessWidget {
|
||||||
|
const SpaceManagementCommunityStructure({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selectedCommunity =
|
||||||
|
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity!;
|
||||||
|
const spacer = Spacer(flex: 10);
|
||||||
|
return Visibility(
|
||||||
|
visible: selectedCommunity.spaces.isNotEmpty,
|
||||||
|
replacement: const Row(
|
||||||
|
children: [spacer, Expanded(child: CreateSpaceButton()), spacer]),
|
||||||
|
child: CommunityStructureCanvas(community: selectedCommunity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class SpaceManagementTemplatesView extends StatelessWidget {
|
||||||
|
const SpaceManagementTemplatesView({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: ColoredBox(
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
child: GridView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 400,
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
childAspectRatio: 2.0,
|
||||||
|
),
|
||||||
|
itemCount: _gridItems(context).length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final model = _gridItems(context)[index];
|
||||||
|
return CommunityTemplateCell(
|
||||||
|
onTap: model.onTap,
|
||||||
|
title: model.title,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_CommunityTemplateModel> _gridItems(BuildContext context) {
|
||||||
|
return [
|
||||||
|
_CommunityTemplateModel(
|
||||||
|
title: const Text('Blank'),
|
||||||
|
onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommunityTemplateModel {
|
||||||
|
final Widget title;
|
||||||
|
final void Function() onTap;
|
||||||
|
|
||||||
|
_CommunityTemplateModel({
|
||||||
|
required this.title,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
}
|
@ -32,7 +32,7 @@ class CommunitiesTreeSelectionBloc
|
|||||||
) {
|
) {
|
||||||
emit(
|
emit(
|
||||||
CommunitiesTreeSelectionState(
|
CommunitiesTreeSelectionState(
|
||||||
selectedCommunity: null,
|
selectedCommunity: event.community,
|
||||||
selectedSpace: event.space,
|
selectedSpace: event.space,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ sealed class CommunitiesTreeSelectionEvent extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
|
final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
|
||||||
final CommunityModel? community;
|
final CommunityModel community;
|
||||||
|
|
||||||
const SelectCommunityEvent({required this.community});
|
const SelectCommunityEvent({required this.community});
|
||||||
@override
|
@override
|
||||||
@ -16,9 +16,10 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent {
|
final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent {
|
||||||
final SpaceModel? space;
|
final SpaceModel space;
|
||||||
|
final CommunityModel community;
|
||||||
|
|
||||||
const SelectSpaceEvent({required this.space});
|
const SelectSpaceEvent({required this.space, required this.community});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [space];
|
List<Object?> get props => [space];
|
||||||
|
@ -30,7 +30,7 @@ class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget {
|
|||||||
initiallyExpanded: spaceIsExpanded,
|
initiallyExpanded: spaceIsExpanded,
|
||||||
onExpansionChanged: (expanded) {},
|
onExpansionChanged: (expanded) {},
|
||||||
onItemSelected: () => context.read<CommunitiesTreeSelectionBloc>().add(
|
onItemSelected: () => context.read<CommunitiesTreeSelectionBloc>().add(
|
||||||
SelectSpaceEvent(space: space),
|
SelectSpaceEvent(community: community, space: space),
|
||||||
),
|
),
|
||||||
children: space.children
|
children: space.children
|
||||||
.map(
|
.map(
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
|
|
||||||
import 'package:syncrow_web/utils/color_manager.dart';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
import 'package:syncrow_web/utils/style.dart';
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
@ -41,7 +40,7 @@ class SpaceManagementSidebarHeader extends StatelessWidget {
|
|||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
_clearSelection(context);
|
_clearSelection(context);
|
||||||
} else {
|
} else {
|
||||||
_showCreateCommunityDialog(context);
|
SpaceManagementCommunityDialogHelper.showCreateDialog(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,19 +49,4 @@ class SpaceManagementSidebarHeader extends StatelessWidget {
|
|||||||
const ClearCommunitiesTreeSelectionEvent(),
|
const ClearCommunitiesTreeSelectionEvent(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCreateCommunityDialog(BuildContext context) => showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => CreateCommunityDialog(
|
|
||||||
title: const Text('Community Name'),
|
|
||||||
onCreateCommunity: (community) {
|
|
||||||
context.read<CommunitiesBloc>().add(
|
|
||||||
InsertCommunity(community),
|
|
||||||
);
|
|
||||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
|
||||||
SelectCommunityEvent(community: community),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ class RemoteCreateCommunityService implements CreateCommunityService {
|
|||||||
return _defaultErrorMessage;
|
return _defaultErrorMessage;
|
||||||
}
|
}
|
||||||
final error = body['error'] as Map<String, dynamic>?;
|
final error = body['error'] as Map<String, dynamic>?;
|
||||||
final errorMessage = error?['error'] as String? ?? '';
|
final errorMessage = error?['message'] as String? ?? '';
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,11 +41,8 @@ class CreateCommunityDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
onCreateCommunity.call(community);
|
onCreateCommunity.call(community);
|
||||||
break;
|
break;
|
||||||
case CreateCommunityFailure(:final message):
|
case CreateCommunityFailure():
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(message)),
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
Reference in New Issue
Block a user