From 99fff948387575ea492ddc508768de9dbd156aa8 Mon Sep 17 00:00:00 2001 From: maiHydrogen Date: Mon, 14 Apr 2025 17:24:43 +0530 Subject: [PATCH 1/9] chore:added some iconbuttons --- assets/images/dashflow_black.png | Bin 0 -> 7204 bytes assets/images/dashflow_blue.png | Bin 0 -> 7730 bytes lib/screens/dashboard.dart | 50 +++++++++++++++++++++++++++++++ pubspec.yaml | 1 + 4 files changed, 51 insertions(+) create mode 100644 assets/images/dashflow_black.png create mode 100644 assets/images/dashflow_blue.png diff --git a/assets/images/dashflow_black.png b/assets/images/dashflow_black.png new file mode 100644 index 0000000000000000000000000000000000000000..a0069f0afb7cdd041751f88e89246e4d35605872 GIT binary patch literal 7204 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 literal 0 HcmV?d00001 diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 428ffaebc..705248f44 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -68,12 +68,62 @@ class Dashboard extends ConsumerWidget { 'History', style: Theme.of(context).textTheme.labelSmall, ), + IconButton( + isSelected: railIdx == 3, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 3; + }, + icon: const Icon(Icons.layers_sharp), + selectedIcon: const Icon(Icons.layers_outlined), + ), + Text( + 'Collections', + style: Theme.of(context).textTheme.labelSmall, + ), + IconButton( + isSelected: railIdx == 4, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 4; + }, + icon: const Icon(Icons.monitor_heart_sharp), + selectedIcon: const Icon(Icons.monitor_heart_outlined), + ), + Text( + 'Monitors', + style: Theme.of(context).textTheme.labelSmall, + ), + IconButton( + isSelected: railIdx == 3, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 3; + }, + icon: Image.asset("assets/images/dashflow_black.png",height: MediaQuery.of(context).size.height*0.05,), + selectedIcon: Image.asset("assets/images/dashflow_blue.png",height: MediaQuery.of(context).size.height*0.05,), + ), + Text( + 'Dash Flow', + style: Theme.of(context).textTheme.labelSmall, + ), ], ), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: NavbarButton( + railIdx: railIdx, + selectedIcon: Icons.play_circle, + icon: Icons.play_circle_outlined, + label: 'Runner', + showLabel: false, + isCompact: true, + onTap: () { + showAboutAppDialog(context); + }, + ), + ), Padding( padding: const EdgeInsets.only(bottom: 16.0), child: NavbarButton( diff --git a/pubspec.yaml b/pubspec.yaml index d4c2ae41a..3334e21c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,3 +94,4 @@ flutter: uses-material-design: true assets: - assets/ + - assets/images/ From 02257757cc622ae38b1b11b4f6bcd45b4a6b5789 Mon Sep 17 00:00:00 2001 From: maiHydrogen Date: Tue, 15 Apr 2025 11:14:19 +0530 Subject: [PATCH 2/9] feat:added grid in workfloe builder --- lib/screens/collections/collections.dart | 26 ++ lib/screens/dashboard.dart | 13 +- lib/screens/dashflow/dashflow.dart | 22 ++ .../dashflow_builder/dashflow_builder.dart | 23 ++ .../dashflow/dashflow_builder/grid.dart | 66 +++++ .../dashflow/dashflow_builder/nodes.dart | 49 ++++ .../dashflow_builder/workflow_canvas.dart | 76 ++++++ lib/screens/dashflow/workflow_pane.dart | 249 ++++++++++++++++++ lib/screens/monitor/monitor.dart | 26 ++ lib/screens/runner/runner.dart | 26 ++ pubspec.lock | 8 + pubspec.yaml | 1 + 12 files changed, 583 insertions(+), 2 deletions(-) create mode 100644 lib/screens/collections/collections.dart create mode 100644 lib/screens/dashflow/dashflow.dart create mode 100644 lib/screens/dashflow/dashflow_builder/dashflow_builder.dart create mode 100644 lib/screens/dashflow/dashflow_builder/grid.dart create mode 100644 lib/screens/dashflow/dashflow_builder/nodes.dart create mode 100644 lib/screens/dashflow/dashflow_builder/workflow_canvas.dart create mode 100644 lib/screens/dashflow/workflow_pane.dart create mode 100644 lib/screens/monitor/monitor.dart create mode 100644 lib/screens/runner/runner.dart diff --git a/lib/screens/collections/collections.dart b/lib/screens/collections/collections.dart new file mode 100644 index 000000000..ce8466e50 --- /dev/null +++ b/lib/screens/collections/collections.dart @@ -0,0 +1,26 @@ +import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart'; +import 'package:apidash/screens/mobile/requests_page/requests_page.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; + +class CollectionPage extends StatelessWidget { + const CollectionPage({super.key}); + + @override + Widget build(BuildContext context) { + return context.isMediumWindow + ? const RequestResponsePage() + : const Column( + children: [ + Expanded( + child: DashboardSplitView( + sidebarWidget: CollectionPane(), + mainWidget: RequestEditorPane(), + ), + ), + ], + ); + } +} diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 705248f44..4ae9cd704 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -1,3 +1,6 @@ +import 'package:apidash/screens/collections/collections.dart'; +import 'package:apidash/screens/dashflow/dashflow.dart'; +import 'package:apidash/screens/monitor/monitor.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -68,6 +71,7 @@ class Dashboard extends ConsumerWidget { 'History', style: Theme.of(context).textTheme.labelSmall, ), + kVSpacer10, IconButton( isSelected: railIdx == 3, onPressed: () { @@ -80,6 +84,7 @@ class Dashboard extends ConsumerWidget { 'Collections', style: Theme.of(context).textTheme.labelSmall, ), + kVSpacer10, IconButton( isSelected: railIdx == 4, onPressed: () { @@ -92,10 +97,11 @@ class Dashboard extends ConsumerWidget { 'Monitors', style: Theme.of(context).textTheme.labelSmall, ), + kVSpacer10, IconButton( - isSelected: railIdx == 3, + isSelected: railIdx == 5, onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 3; + ref.read(navRailIndexStateProvider.notifier).state = 5; }, icon: Image.asset("assets/images/dashflow_black.png",height: MediaQuery.of(context).size.height*0.05,), selectedIcon: Image.asset("assets/images/dashflow_blue.png",height: MediaQuery.of(context).size.height*0.05,), @@ -168,6 +174,9 @@ class Dashboard extends ConsumerWidget { HomePage(), EnvironmentPage(), HistoryPage(), + CollectionPage(), + MonitorPage(), + DashflowPage(), SettingsPage(), ], ), diff --git a/lib/screens/dashflow/dashflow.dart b/lib/screens/dashflow/dashflow.dart new file mode 100644 index 000000000..548a7d4b4 --- /dev/null +++ b/lib/screens/dashflow/dashflow.dart @@ -0,0 +1,22 @@ +import 'package:apidash/screens/dashflow/dashflow_builder/dashflow_builder.dart'; +import 'package:apidash/screens/dashflow/workflow_pane.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; + +class DashflowPage extends StatelessWidget { + const DashflowPage({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: DashboardSplitView( + sidebarWidget: WorkflowPane(), + mainWidget: DashflowBuilder(), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/dashflow/dashflow_builder/dashflow_builder.dart b/lib/screens/dashflow/dashflow_builder/dashflow_builder.dart new file mode 100644 index 000000000..0b6fe8405 --- /dev/null +++ b/lib/screens/dashflow/dashflow_builder/dashflow_builder.dart @@ -0,0 +1,23 @@ +import 'package:apidash/screens/dashflow/dashflow_builder/workflow_canvas.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; + +class DashflowBuilder extends ConsumerWidget { + const DashflowBuilder({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + if (selectedId == null) { + return WorkflowCanvas(); + } else { + return WorkflowCanvas(); + } + } +} + + + diff --git a/lib/screens/dashflow/dashflow_builder/grid.dart b/lib/screens/dashflow/dashflow_builder/grid.dart new file mode 100644 index 000000000..bb78572ec --- /dev/null +++ b/lib/screens/dashflow/dashflow_builder/grid.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:path_drawing/path_drawing.dart'; + +class GridPainter extends CustomPainter { + final Matrix4 transformation; + final Size viewportSize; + final double baseGridSize; + + GridPainter({ + required this.transformation, + required this.viewportSize, + this.baseGridSize = 20, + }); + + @override + void paint(Canvas canvas, Size size) { + final zoom = transformation.getMaxScaleOnAxis(); + final gridSize = baseGridSize * zoom; + + final dx = transformation.getTranslation().x; + final dy = transformation.getTranslation().y; + + // Viewport-relative offsets + final startX = -dx % gridSize; + final startY = -dy % gridSize; + + final cols = (viewportSize.width / gridSize).ceil() + 2; + final rows = (viewportSize.height / gridSize).ceil() + 2; + + final paint = Paint() + ..color = Colors.black + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + + // Restrict drawing to visible area + canvas + .clipRect(Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height)); + + // Draw vertical dotted lines + for (int i = 0; i < cols; i++) { + final x = startX + i * gridSize; + final path = Path() + ..moveTo(x, 0) + ..lineTo(x, viewportSize.height); + + canvas.drawPath(dashPath(path, dashArray: CircularIntervalList([1,5])), paint); + } + + // Draw horizontal dotted lines + for (int j = 0; j < rows; j++) { + final y = startY + j * gridSize; + final path = Path() + ..moveTo(0, y) + ..lineTo(viewportSize.width, y); + + canvas.drawPath(dashPath(path, dashArray: CircularIntervalList([1,5])), paint); + } + + } + + @override + bool shouldRepaint(covariant GridPainter oldDelegate) => + oldDelegate.transformation != transformation || + oldDelegate.viewportSize != viewportSize; +} + diff --git a/lib/screens/dashflow/dashflow_builder/nodes.dart b/lib/screens/dashflow/dashflow_builder/nodes.dart new file mode 100644 index 000000000..b506df908 --- /dev/null +++ b/lib/screens/dashflow/dashflow_builder/nodes.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class NodeData { + final int id; + final Offset offset; + + NodeData({required this.id, required this.offset}); + + NodeData copyWith({Offset? offset}) => + NodeData(id: id, offset: offset ?? this.offset); +} + +class DraggableNode extends StatefulWidget { + final NodeData node; + final Function(int id, Offset newOffset) onDrag; + + const DraggableNode({super.key, required this.node, required this.onDrag}); + + @override + State createState() => _DraggableNodeState(); +} + +class _DraggableNodeState extends State { + Offset offset = Offset.zero; + + @override + void initState() { + super.initState(); + offset = widget.node.offset; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onPanUpdate: (details) { + offset += details.delta; + widget.onDrag(widget.node.id, offset); + }, + child: Card( + elevation: 4, + color: Colors.lightBlue[100], + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text("Node ${widget.node.id}"), + ), + ), + ); + } +} diff --git a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart new file mode 100644 index 000000000..052840107 --- /dev/null +++ b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart @@ -0,0 +1,76 @@ +import 'package:apidash/screens/dashflow/dashflow_builder/grid.dart'; +import 'package:apidash/screens/dashflow/dashflow_builder/nodes.dart'; +import 'package:flutter/material.dart'; + +class WorkflowCanvas extends StatefulWidget { + const WorkflowCanvas({super.key}); + + @override + State createState() => _WorkflowCanvasState(); +} + +class _WorkflowCanvasState extends State { + final TransformationController _controller = TransformationController(); + + List nodes = [ + NodeData(id: 1, offset: Offset(100,100)), + NodeData(id: 2, offset: Offset(150,150)), + ]; + + void _onNodeDrag(int id, Offset newOffset) { + final index = nodes.indexWhere((n) => n.id == id); + if (index != -1) { + setState(() { + nodes[index] = nodes[index].copyWith(offset: newOffset); + }); + } + } + + @override + Widget build(BuildContext context) { + final mq = MediaQuery.of(context).size; + return Scaffold( + appBar: AppBar(title: Text("New Dashflow")), + body: Padding( + padding: const EdgeInsets.only(left: 5,right: 5,bottom: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)), + color: const Color.fromARGB(255, 229, 247, 255), + ), + child: InteractiveViewer( + transformationController: _controller, + constrained: false, + boundaryMargin: EdgeInsets.all(double.infinity), + minScale: 0.5, + maxScale: 3, + child: SizedBox( + height: mq.width, + width: mq.width, + child: CustomPaint( + painter: GridPainter( + transformation: _controller.value, + viewportSize: Size(mq.width * 10, + mq.width * 10), + ), + child: Stack( + children: nodes.map((node) { + return Positioned( + left: node.offset.dx, + top: node.offset.dy, + child: DraggableNode( + node: node, + onDrag: _onNodeDrag, + ), + ); + }).toList(), + ), + ), + ), + ), + ), + ), + ); + } +} + diff --git a/lib/screens/dashflow/workflow_pane.dart b/lib/screens/dashflow/workflow_pane.dart new file mode 100644 index 000000000..22f0c062e --- /dev/null +++ b/lib/screens/dashflow/workflow_pane.dart @@ -0,0 +1,249 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/importer/import_dialog.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; +import '../common_widgets/common_widgets.dart'; + +class WorkflowPane extends ConsumerWidget { + const WorkflowPane({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final collection = ref.watch(collectionStateNotifierProvider); + var sm = ScaffoldMessenger.of(context); + if (collection == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return Padding( + padding: (!context.isMediumWindow && kIsMacOS ? kPt24l4 : kPt8l4) + + (context.isMediumWindow ? kPb70 : EdgeInsets.zero), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SidebarHeader( + onAddNew: () { + ref.read(collectionStateNotifierProvider.notifier).add(); + }, + onImport: () { + importToCollectionPane(context, ref, sm); + }, + ), + if (context.isMediumWindow) kVSpacer6, + if (context.isMediumWindow) + Padding( + padding: kPh8, + child: EnvironmentDropdown(), + ), + kVSpacer10, + SidebarFilter( + filterHintText: "Filter by name or url", + onFilterFieldChanged: (value) { + ref.read(collectionSearchQueryProvider.notifier).state = + value.toLowerCase(); + }, + ), + kVSpacer10, + const Expanded( + child: RequestList(), + ), + kVSpacer5 + ], + ), + ); + } +} + +class RequestList extends ConsumerStatefulWidget { + const RequestList({ + super.key, + }); + + @override + ConsumerState createState() => _RequestListState(); +} + +class _RequestListState extends ConsumerState { + late final ScrollController controller; + + @override + void initState() { + super.initState(); + controller = ScrollController(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final requestSequence = ref.watch(requestSequenceProvider); + final requestItems = ref.watch(collectionStateNotifierProvider)!; + final alwaysShowCollectionPaneScrollbar = ref.watch(settingsProvider + .select((value) => value.alwaysShowCollectionPaneScrollbar)); + final filterQuery = ref.watch(collectionSearchQueryProvider).trim(); + + return Scrollbar( + controller: controller, + thumbVisibility: alwaysShowCollectionPaneScrollbar ? true : null, + radius: const Radius.circular(12), + child: filterQuery.isEmpty + ? ReorderableListView.builder( + padding: context.isMediumWindow + ? EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + right: 8, + ) + : kPe8, + scrollController: controller, + buildDefaultDragHandles: false, + itemCount: requestSequence.length, + onReorder: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + if (oldIndex != newIndex) { + ref + .read(collectionStateNotifierProvider.notifier) + .reorder(oldIndex, newIndex); + } + }, + itemBuilder: (context, index) { + var id = requestSequence[index]; + if (kIsMobile) { + return ReorderableDelayedDragStartListener( + key: ValueKey(id), + index: index, + child: Padding( + padding: kP1, + child: RequestItem( + id: id, + requestModel: requestItems[id]!, + ), + ), + ); + } + return ReorderableDragStartListener( + key: ValueKey(id), + index: index, + child: Padding( + padding: kP1, + child: RequestItem( + id: id, + requestModel: requestItems[id]!, + ), + ), + ); + }, + ) + : ListView( + padding: context.isMediumWindow + ? EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + right: 8, + ) + : kPe8, + controller: controller, + children: requestSequence.map((id) { + var item = requestItems[id]!; + if (item.httpRequestModel!.url + .toLowerCase() + .contains(filterQuery) || + item.name.toLowerCase().contains(filterQuery)) { + return Padding( + padding: kP1, + child: RequestItem( + id: id, + requestModel: item, + ), + ); + } + return kSizedBoxEmpty; + }).toList(), + ), + ); + } +} + +class RequestItem extends ConsumerWidget { + const RequestItem({ + super.key, + required this.id, + required this.requestModel, + }); + + final String id; + final RequestModel requestModel; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final editRequestId = ref.watch(selectedIdEditStateProvider); + + return SidebarRequestCard( + id: id, + apiType: requestModel.apiType, + method: requestModel.httpRequestModel!.method, + name: requestModel.name, + url: requestModel.httpRequestModel?.url, + selectedId: selectedId, + editRequestId: editRequestId, + onTap: () { + ref.read(selectedIdStateProvider.notifier).state = id; + kHomeScaffoldKey.currentState?.closeDrawer(); + }, + onSecondaryTap: () { + ref.read(selectedIdStateProvider.notifier).state = id; + }, + // onDoubleTap: () { + // ref.read(selectedIdStateProvider.notifier).state = id; + // ref.read(selectedIdEditStateProvider.notifier).state = id; + // }, + // controller: ref.watch(nameTextFieldControllerProvider), + focusNode: ref.watch(nameTextFieldFocusNodeProvider), + onChangedNameEditor: (value) { + value = value.trim(); + ref + .read(collectionStateNotifierProvider.notifier) + .update(id: editRequestId!, name: value); + }, + onTapOutsideNameEditor: () { + ref.read(selectedIdEditStateProvider.notifier).state = null; + }, + onMenuSelected: (ItemMenuOption item) { + if (item == ItemMenuOption.edit) { + // var controller = + // ref.read(nameTextFieldControllerProvider.notifier).state; + // controller.text = requestModel.name; + // controller.selection = TextSelection.fromPosition( + // TextPosition(offset: controller.text.length), + // ); + ref.read(selectedIdEditStateProvider.notifier).state = id; + Future.delayed( + const Duration(milliseconds: 150), + () => ref + .read(nameTextFieldFocusNodeProvider.notifier) + .state + .requestFocus(), + ); + } + if (item == ItemMenuOption.delete) { + ref.read(collectionStateNotifierProvider.notifier).remove(id: id); + } + if (item == ItemMenuOption.duplicate) { + ref.read(collectionStateNotifierProvider.notifier).duplicate(id: id); + } + }, + ); + } +} diff --git a/lib/screens/monitor/monitor.dart b/lib/screens/monitor/monitor.dart new file mode 100644 index 000000000..8875715cb --- /dev/null +++ b/lib/screens/monitor/monitor.dart @@ -0,0 +1,26 @@ +import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart'; +import 'package:apidash/screens/mobile/requests_page/requests_page.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; + +class MonitorPage extends StatelessWidget { + const MonitorPage({super.key}); + + @override + Widget build(BuildContext context) { + return context.isMediumWindow + ? const RequestResponsePage() + : const Column( + children: [ + Expanded( + child: DashboardSplitView( + sidebarWidget: CollectionPane(), + mainWidget: RequestEditorPane(), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/runner/runner.dart b/lib/screens/runner/runner.dart new file mode 100644 index 000000000..4bcfd990a --- /dev/null +++ b/lib/screens/runner/runner.dart @@ -0,0 +1,26 @@ +import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart'; +import 'package:apidash/screens/mobile/requests_page/requests_page.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; + +class RunnerPage extends StatelessWidget { + const RunnerPage({super.key}); + + @override + Widget build(BuildContext context) { + return context.isMediumWindow + ? const RequestResponsePage() + : const Column( + children: [ + Expanded( + child: DashboardSplitView( + sidebarWidget: CollectionPane(), + mainWidget: RequestEditorPane(), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 9881558db..8b2946add 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1080,6 +1080,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_drawing: + dependency: "direct main" + description: + name: path_drawing + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_parsing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 44c528cac..1f3e49716 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: url: https://github.com/google/flutter-desktop-embedding.git path: plugins/window_size carousel_slider: ^5.0.0 + path_drawing: ^1.0.1 dependency_overrides: extended_text_field: ^16.0.0 From c7b3fcc7cf4dc2ad3e1eeb6b5a2e070c89094559 Mon Sep 17 00:00:00 2001 From: maiHydrogen Date: Tue, 15 Apr 2025 21:53:14 +0530 Subject: [PATCH 3/9] chore: enhanced the grid plotting --- .../dashflow/dashflow_builder/grid.dart | 70 ++++++++++++------- .../dashflow_builder/workflow_canvas.dart | 3 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/lib/screens/dashflow/dashflow_builder/grid.dart b/lib/screens/dashflow/dashflow_builder/grid.dart index bb78572ec..6b9b288b5 100644 --- a/lib/screens/dashflow/dashflow_builder/grid.dart +++ b/lib/screens/dashflow/dashflow_builder/grid.dart @@ -1,5 +1,7 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_drawing/path_drawing.dart'; +import 'package:vector_math/vector_math_64.dart' as vm; class GridPainter extends CustomPainter { final Matrix4 transformation; @@ -11,21 +13,33 @@ class GridPainter extends CustomPainter { required this.viewportSize, this.baseGridSize = 20, }); + Offset _transformPoint(Offset point, Matrix4 matrix) { + final vector = matrix.transform3(vm.Vector3(point.dx, point.dy, 0)); + return Offset(vector.x, vector.y); + } @override void paint(Canvas canvas, Size size) { final zoom = transformation.getMaxScaleOnAxis(); final gridSize = baseGridSize * zoom; + final inverted = Matrix4.copy(transformation)..invert(); final dx = transformation.getTranslation().x; final dy = transformation.getTranslation().y; + // Calculate visible top-left and bottom-right corners in world space + final topLeft = _transformPoint(Offset.zero, inverted); + final bottomRight = _transformPoint( + Offset(viewportSize.width, viewportSize.height), + inverted, + ); + // Viewport-relative offsets - final startX = -dx % gridSize; - final startY = -dy % gridSize; + final startX = (topLeft.dx ~/ baseGridSize - 1) * baseGridSize.toDouble(); + final endX = (bottomRight.dx ~/ baseGridSize + 1) * baseGridSize.toDouble(); - final cols = (viewportSize.width / gridSize).ceil() + 2; - final rows = (viewportSize.height / gridSize).ceil() + 2; + final startY = (topLeft.dy ~/ baseGridSize - 1) * baseGridSize.toDouble(); + final endY = (bottomRight.dy ~/ baseGridSize + 1) * baseGridSize.toDouble(); final paint = Paint() ..color = Colors.black @@ -33,34 +47,38 @@ class GridPainter extends CustomPainter { ..strokeWidth = 1; // Restrict drawing to visible area - canvas - .clipRect(Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height)); + //canvas + // .clipRect(Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height)); - // Draw vertical dotted lines - for (int i = 0; i < cols; i++) { - final x = startX + i * gridSize; + // Draw vertical dotted lines + // Vertical grid lines + for (double x = startX; x <= endX; x += baseGridSize) { final path = Path() - ..moveTo(x, 0) - ..lineTo(x, viewportSize.height); + ..moveTo(x, startY) + ..lineTo(x, endY); - canvas.drawPath(dashPath(path, dashArray: CircularIntervalList([1,5])), paint); + canvas.drawPath( + dashPath(path, dashArray: CircularIntervalList([2, 6])), + paint, + ); } - // Draw horizontal dotted lines - for (int j = 0; j < rows; j++) { - final y = startY + j * gridSize; +// Horizontal grid lines + for (double y = startY; y <= endY; y += baseGridSize) { final path = Path() - ..moveTo(0, y) - ..lineTo(viewportSize.width, y); + ..moveTo(startX, y) + ..lineTo(endX, y); - canvas.drawPath(dashPath(path, dashArray: CircularIntervalList([1,5])), paint); + canvas.drawPath( + dashPath(path, dashArray: CircularIntervalList([2, 6])), + paint, + ); } - - } - - @override - bool shouldRepaint(covariant GridPainter oldDelegate) => - oldDelegate.transformation != transformation || - oldDelegate.viewportSize != viewportSize; } - + @override + bool shouldRepaint( GridPainter oldDelegate) { + return !listEquals( + oldDelegate.transformation.storage, transformation.storage) || + oldDelegate.viewportSize != viewportSize; + } +} \ No newline at end of file diff --git a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart index 052840107..09d8c25d8 100644 --- a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart +++ b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart @@ -50,8 +50,7 @@ class _WorkflowCanvasState extends State { child: CustomPaint( painter: GridPainter( transformation: _controller.value, - viewportSize: Size(mq.width * 10, - mq.width * 10), + viewportSize: MediaQuery.of(context).size, ), child: Stack( children: nodes.map((node) { diff --git a/pubspec.lock b/pubspec.lock index 8b2946add..7546654e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1749,7 +1749,7 @@ packages: source: hosted version: "1.1.16" vector_math: - dependency: transitive + dependency: "direct main" description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" diff --git a/pubspec.yaml b/pubspec.yaml index 1f3e49716..a64c7c40c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: path: plugins/window_size carousel_slider: ^5.0.0 path_drawing: ^1.0.1 + vector_math: ^2.1.4 dependency_overrides: extended_text_field: ^16.0.0 From 44315a198cd336f9174fbaeb297e41368ae6a231 Mon Sep 17 00:00:00 2001 From: maiHydrogen Date: Thu, 17 Apr 2025 10:10:11 +0530 Subject: [PATCH 4/9] chore:tried some fixes for grid --- lib/screens/dashflow/dashflow_builder/grid.dart | 14 +++++--------- .../dashflow/dashflow_builder/workflow_canvas.dart | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/screens/dashflow/dashflow_builder/grid.dart b/lib/screens/dashflow/dashflow_builder/grid.dart index 6b9b288b5..b0b87467c 100644 --- a/lib/screens/dashflow/dashflow_builder/grid.dart +++ b/lib/screens/dashflow/dashflow_builder/grid.dart @@ -11,7 +11,7 @@ class GridPainter extends CustomPainter { GridPainter({ required this.transformation, required this.viewportSize, - this.baseGridSize = 20, + this.baseGridSize = 50, }); Offset _transformPoint(Offset point, Matrix4 matrix) { final vector = matrix.transform3(vm.Vector3(point.dx, point.dy, 0)); @@ -30,7 +30,7 @@ class GridPainter extends CustomPainter { // Calculate visible top-left and bottom-right corners in world space final topLeft = _transformPoint(Offset.zero, inverted); final bottomRight = _transformPoint( - Offset(viewportSize.width, viewportSize.height), + Offset(viewportSize.width, viewportSize.width), inverted, ); @@ -42,14 +42,10 @@ class GridPainter extends CustomPainter { final endY = (bottomRight.dy ~/ baseGridSize + 1) * baseGridSize.toDouble(); final paint = Paint() - ..color = Colors.black + ..color = Colors.blueGrey ..style = PaintingStyle.stroke ..strokeWidth = 1; - // Restrict drawing to visible area - //canvas - // .clipRect(Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height)); - // Draw vertical dotted lines // Vertical grid lines for (double x = startX; x <= endX; x += baseGridSize) { @@ -58,7 +54,7 @@ class GridPainter extends CustomPainter { ..lineTo(x, endY); canvas.drawPath( - dashPath(path, dashArray: CircularIntervalList([2, 6])), + dashPath(path, dashArray: CircularIntervalList([1, 5])), paint, ); } @@ -70,7 +66,7 @@ class GridPainter extends CustomPainter { ..lineTo(endX, y); canvas.drawPath( - dashPath(path, dashArray: CircularIntervalList([2, 6])), + dashPath(path, dashArray: CircularIntervalList([1, 5])), paint, ); } diff --git a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart index 09d8c25d8..480fe8d88 100644 --- a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart +++ b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart @@ -42,7 +42,7 @@ class _WorkflowCanvasState extends State { transformationController: _controller, constrained: false, boundaryMargin: EdgeInsets.all(double.infinity), - minScale: 0.5, + minScale: 1, maxScale: 3, child: SizedBox( height: mq.width, From 2ee2d17ae8d82a8506a5bed8c48e3f01a0459149 Mon Sep 17 00:00:00 2001 From: HydrogenIITG Date: Sat, 19 Apr 2025 06:24:55 +0530 Subject: [PATCH 5/9] fix: resolved bugs in grid building --- lib/providers/ui_providers.dart | 17 +++++ .../dashflow/dashflow_builder/grid.dart | 50 +++++++------ .../dashflow/dashflow_builder/nodes.dart | 22 +++++- .../dashflow_builder/workflow_canvas.dart | 72 +++++++++++++------ packages/apidash_core/pubspec_overrides.yaml | 8 +-- 5 files changed, 118 insertions(+), 51 deletions(-) diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 4e5d217f5..573f318ae 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -1,6 +1,7 @@ import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/screens/dashflow/dashflow_builder/nodes.dart'; final mobileScaffoldKeyStateProvider = StateProvider>((ref) => kHomeScaffoldKey); @@ -37,3 +38,19 @@ final environmentSearchQueryProvider = StateProvider((ref) => ''); final importFormatStateProvider = StateProvider((ref) => ImportFormat.curl); final userOnboardedProvider = StateProvider((ref) => false); + +final workflowProvider = StateNotifierProvider>((ref) => WorkflowNotifier()); + +class WorkflowNotifier extends StateNotifier> { + WorkflowNotifier() : super([ + NodeData(id: 1, offset: Offset(0, 0)), + NodeData(id: 2, offset: Offset(50, 50)), + ]); + + void updateNodeOffset(int id, Offset newOffset) { + state = [ + for (final node in state) + if (node.id == id) node.copyWith(offset: newOffset) else node, + ]; + } +} \ No newline at end of file diff --git a/lib/screens/dashflow/dashflow_builder/grid.dart b/lib/screens/dashflow/dashflow_builder/grid.dart index b0b87467c..8d44439b2 100644 --- a/lib/screens/dashflow/dashflow_builder/grid.dart +++ b/lib/screens/dashflow/dashflow_builder/grid.dart @@ -7,11 +7,13 @@ class GridPainter extends CustomPainter { final Matrix4 transformation; final Size viewportSize; final double baseGridSize; + final double canvasSize; GridPainter({ required this.transformation, required this.viewportSize, this.baseGridSize = 50, + required this.canvasSize, }); Offset _transformPoint(Offset point, Matrix4 matrix) { final vector = matrix.transform3(vm.Vector3(point.dx, point.dy, 0)); @@ -27,31 +29,33 @@ class GridPainter extends CustomPainter { final dx = transformation.getTranslation().x; final dy = transformation.getTranslation().y; - // Calculate visible top-left and bottom-right corners in world space - final topLeft = _transformPoint(Offset.zero, inverted); - final bottomRight = _transformPoint( - Offset(viewportSize.width, viewportSize.width), - inverted, - ); + // Increase grid spacing at low zoom to reduce lines + final effectiveGridSize = zoom < 1 ? gridSize * 2 : gridSize; + + // Center the origin + final centerOffset = Offset(canvasSize / 2, canvasSize / 2); + final topLeft = _transformPoint(Offset.zero, inverted) - centerOffset; + final bottomRight = + _transformPoint(Offset(size.width, size.height), inverted) - + centerOffset; // Viewport-relative offsets - final startX = (topLeft.dx ~/ baseGridSize - 1) * baseGridSize.toDouble(); - final endX = (bottomRight.dx ~/ baseGridSize + 1) * baseGridSize.toDouble(); + final startX = (topLeft.dx ~/ effectiveGridSize - 1) * effectiveGridSize.toDouble(); + final endX = (bottomRight.dx ~/ effectiveGridSize + 1) * effectiveGridSize.toDouble(); - final startY = (topLeft.dy ~/ baseGridSize - 1) * baseGridSize.toDouble(); - final endY = (bottomRight.dy ~/ baseGridSize + 1) * baseGridSize.toDouble(); + final startY = (topLeft.dy ~/ effectiveGridSize - 1) * effectiveGridSize.toDouble(); + final endY = (bottomRight.dy ~/ effectiveGridSize + 1) * effectiveGridSize.toDouble(); final paint = Paint() - ..color = Colors.blueGrey + ..color = Colors.blueGrey.shade600 ..style = PaintingStyle.stroke ..strokeWidth = 1; // Draw vertical dotted lines - // Vertical grid lines for (double x = startX; x <= endX; x += baseGridSize) { final path = Path() - ..moveTo(x, startY) - ..lineTo(x, endY); + ..moveTo(x + centerOffset.dx, startY + centerOffset.dy) + ..lineTo(x + centerOffset.dx, endY + centerOffset.dy); canvas.drawPath( dashPath(path, dashArray: CircularIntervalList([1, 5])), @@ -62,19 +66,19 @@ class GridPainter extends CustomPainter { // Horizontal grid lines for (double y = startY; y <= endY; y += baseGridSize) { final path = Path() - ..moveTo(startX, y) - ..lineTo(endX, y); - + ..moveTo(startX + centerOffset.dx, y + centerOffset.dy) + ..lineTo(endX + centerOffset.dx, y + centerOffset.dy); canvas.drawPath( dashPath(path, dashArray: CircularIntervalList([1, 5])), paint, ); } -} + } + @override - bool shouldRepaint( GridPainter oldDelegate) { - return !listEquals( - oldDelegate.transformation.storage, transformation.storage) || - oldDelegate.viewportSize != viewportSize; + bool shouldRepaint(GridPainter oldDelegate) { + return transformation != oldDelegate.transformation || + viewportSize != oldDelegate.viewportSize || + canvasSize != oldDelegate.canvasSize; } -} \ No newline at end of file +} diff --git a/lib/screens/dashflow/dashflow_builder/nodes.dart b/lib/screens/dashflow/dashflow_builder/nodes.dart index b506df908..c9c3a3dec 100644 --- a/lib/screens/dashflow/dashflow_builder/nodes.dart +++ b/lib/screens/dashflow/dashflow_builder/nodes.dart @@ -21,7 +21,8 @@ class DraggableNode extends StatefulWidget { } class _DraggableNodeState extends State { - Offset offset = Offset.zero; + late Offset offset; + bool isDragging = false; @override void initState() { @@ -32,16 +33,31 @@ class _DraggableNodeState extends State { @override Widget build(BuildContext context) { return GestureDetector( + onPanStart:(_){ + setState((){ + isDragging =true; + }); + }, onPanUpdate: (details) { - offset += details.delta; + if(isDragging){ + final zoom = (context.findAncestorWidgetOfExactType()?.transformationController?.value.getMaxScaleOnAxis() ?? 1.0); + final adjustedDelta = details.delta*zoom; + offset += adjustedDelta; widget.onDrag(widget.node.id, offset); + } + }, + onPanEnd: (_){ + setState((){ + isDragging = false; + }); }, child: Card( elevation: 4, color: Colors.lightBlue[100], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(16.0), - child: Text("Node ${widget.node.id}"), + child: Text("Node ${widget.node.id}", style: Theme.of(context).textTheme.bodyMedium), ), ), ); diff --git a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart index 480fe8d88..ad00f19f0 100644 --- a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart +++ b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart @@ -1,34 +1,56 @@ import 'package:apidash/screens/dashflow/dashflow_builder/grid.dart'; import 'package:apidash/screens/dashflow/dashflow_builder/nodes.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/widgets.dart'; -class WorkflowCanvas extends StatefulWidget { +class WorkflowCanvas extends ConsumerStatefulWidget { const WorkflowCanvas({super.key}); @override - State createState() => _WorkflowCanvasState(); + ConsumerState createState() => _WorkflowCanvasState(); } -class _WorkflowCanvasState extends State { +class _WorkflowCanvasState extends ConsumerState { final TransformationController _controller = TransformationController(); + bool _needsRepaint = false; - List nodes = [ - NodeData(id: 1, offset: Offset(100,100)), - NodeData(id: 2, offset: Offset(150,150)), - ]; + @override + void initState() { + super.initState(); + const canvasSize = 5000.0; + _controller.value = Matrix4.identity() + ..translate(-canvasSize / 2 + 100, -canvasSize / 2 + 100); + _controller.addListener(_onTransformChanged); + } - void _onNodeDrag(int id, Offset newOffset) { - final index = nodes.indexWhere((n) => n.id == id); - if (index != -1) { - setState(() { - nodes[index] = nodes[index].copyWith(offset: newOffset); - }); + @override + void dispose() { + _controller.removeListener(_onTransformChanged); + _controller.dispose(); + super.dispose(); + } + + void _onTransformChanged() { + if (!_needsRepaint){ + _needsRepaint = true; + WidgetsBinding.instance.addPostFrameCallback((_){ + if (mounted){ + setState((){ + _needsRepaint = false; + }); + } } + ); + } } @override Widget build(BuildContext context) { final mq = MediaQuery.of(context).size; + const canvasSize = 5000.00; + final nodes = ref.watch(workflowProvider); return Scaffold( appBar: AppBar(title: Text("New Dashflow")), body: Padding( @@ -41,25 +63,33 @@ class _WorkflowCanvasState extends State { child: InteractiveViewer( transformationController: _controller, constrained: false, - boundaryMargin: EdgeInsets.all(double.infinity), - minScale: 1, + boundaryMargin: EdgeInsets.all(canvasSize/2), + minScale: 0.5, maxScale: 3, child: SizedBox( - height: mq.width, - width: mq.width, + height: canvasSize, + width: canvasSize, child: CustomPaint( painter: GridPainter( + canvasSize: canvasSize, transformation: _controller.value, - viewportSize: MediaQuery.of(context).size, + viewportSize: mq, ), child: Stack( + clipBehavior: Clip.none, children: nodes.map((node) { + // Translate node positions to center of canvas + final centeredOffset = Offset( + node.offset.dx + canvasSize / 2, + node.offset.dy + canvasSize / 2, + ); return Positioned( - left: node.offset.dx, - top: node.offset.dy, + left: centeredOffset.dx, + top: centeredOffset.dy, child: DraggableNode( node: node, - onDrag: _onNodeDrag, + onDrag:(id, offset) => + ref.read(workflowProvider.notifier).updateNodeOffset(id, offset), ), ); }).toList(), diff --git a/packages/apidash_core/pubspec_overrides.yaml b/packages/apidash_core/pubspec_overrides.yaml index 61081fc76..9ed8c31bf 100644 --- a/packages/apidash_core/pubspec_overrides.yaml +++ b/packages/apidash_core/pubspec_overrides.yaml @@ -1,10 +1,10 @@ # melos_managed_dependency_overrides: curl_parser,insomnia_collection,postman,seed dependency_overrides: curl_parser: - path: ../curl_parser + path: ..\\curl_parser insomnia_collection: - path: ../insomnia_collection + path: ..\\insomnia_collection postman: - path: ../postman + path: ..\\postman seed: - path: ../seed + path: ..\\seed From 7b8786f0c8f27c6760e3c3e4dbc6cc624eab3020 Mon Sep 17 00:00:00 2001 From: HydrogenIITG Date: Sat, 19 Apr 2025 18:16:22 +0530 Subject: [PATCH 6/9] feat:grid is completed --- .../dashflow/dashflow_builder/grid.dart | 60 ++++++++----------- .../dashflow/dashflow_builder/nodes.dart | 10 +++- .../dashflow_builder/workflow_canvas.dart | 47 +++++++++------ pubspec.lock | 8 --- pubspec.yaml | 1 - 5 files changed, 62 insertions(+), 64 deletions(-) diff --git a/lib/screens/dashflow/dashflow_builder/grid.dart b/lib/screens/dashflow/dashflow_builder/grid.dart index 8d44439b2..b7fc5c144 100644 --- a/lib/screens/dashflow/dashflow_builder/grid.dart +++ b/lib/screens/dashflow/dashflow_builder/grid.dart @@ -1,6 +1,4 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:path_drawing/path_drawing.dart'; import 'package:vector_math/vector_math_64.dart' as vm; class GridPainter extends CustomPainter { @@ -12,7 +10,7 @@ class GridPainter extends CustomPainter { GridPainter({ required this.transformation, required this.viewportSize, - this.baseGridSize = 50, + this.baseGridSize = 20, required this.canvasSize, }); Offset _transformPoint(Offset point, Matrix4 matrix) { @@ -26,11 +24,12 @@ class GridPainter extends CustomPainter { final gridSize = baseGridSize * zoom; final inverted = Matrix4.copy(transformation)..invert(); - final dx = transformation.getTranslation().x; - final dy = transformation.getTranslation().y; - // Increase grid spacing at low zoom to reduce lines - final effectiveGridSize = zoom < 1 ? gridSize * 2 : gridSize; + final effectiveGridSize = zoom < 0.5 + ? gridSize * 4 + : zoom < 1 + ? gridSize * 2 + : gridSize; // Center the origin final centerOffset = Offset(canvasSize / 2, canvasSize / 2); @@ -40,38 +39,29 @@ class GridPainter extends CustomPainter { centerOffset; // Viewport-relative offsets - final startX = (topLeft.dx ~/ effectiveGridSize - 1) * effectiveGridSize.toDouble(); - final endX = (bottomRight.dx ~/ effectiveGridSize + 1) * effectiveGridSize.toDouble(); + final startX = + (topLeft.dx ~/ effectiveGridSize - 1) * effectiveGridSize.toDouble(); + final endX = (bottomRight.dx ~/ effectiveGridSize + 1) * + effectiveGridSize.toDouble(); - final startY = (topLeft.dy ~/ effectiveGridSize - 1) * effectiveGridSize.toDouble(); - final endY = (bottomRight.dy ~/ effectiveGridSize + 1) * effectiveGridSize.toDouble(); + final startY = + (topLeft.dy ~/ effectiveGridSize - 1) * effectiveGridSize.toDouble(); + final endY = (bottomRight.dy ~/ effectiveGridSize + 1) * + effectiveGridSize.toDouble(); final paint = Paint() - ..color = Colors.blueGrey.shade600 - ..style = PaintingStyle.stroke - ..strokeWidth = 1; - - // Draw vertical dotted lines - for (double x = startX; x <= endX; x += baseGridSize) { - final path = Path() - ..moveTo(x + centerOffset.dx, startY + centerOffset.dy) - ..lineTo(x + centerOffset.dx, endY + centerOffset.dy); - - canvas.drawPath( - dashPath(path, dashArray: CircularIntervalList([1, 5])), - paint, - ); - } + ..color = Colors.blue.shade700 + ..style = PaintingStyle.fill; -// Horizontal grid lines - for (double y = startY; y <= endY; y += baseGridSize) { - final path = Path() - ..moveTo(startX + centerOffset.dx, y + centerOffset.dy) - ..lineTo(endX + centerOffset.dx, y + centerOffset.dy); - canvas.drawPath( - dashPath(path, dashArray: CircularIntervalList([1, 5])), - paint, - ); + // Draw dots at grid box corners + for (double x = startX; x <= endX; x += effectiveGridSize) { + for (double y = startY; y <= endY; y += effectiveGridSize) { + canvas.drawCircle( + Offset(x + centerOffset.dx, y + centerOffset.dy), + 1.0, + paint, + ); + } } } diff --git a/lib/screens/dashflow/dashflow_builder/nodes.dart b/lib/screens/dashflow/dashflow_builder/nodes.dart index c9c3a3dec..937aaeb75 100644 --- a/lib/screens/dashflow/dashflow_builder/nodes.dart +++ b/lib/screens/dashflow/dashflow_builder/nodes.dart @@ -13,8 +13,9 @@ class NodeData { class DraggableNode extends StatefulWidget { final NodeData node; final Function(int id, Offset newOffset) onDrag; + final double gridSize; // Added for snap-to-grid - const DraggableNode({super.key, required this.node, required this.onDrag}); + const DraggableNode({super.key, required this.node, required this.onDrag,required this.gridSize,}); @override State createState() => _DraggableNodeState(); @@ -43,7 +44,12 @@ class _DraggableNodeState extends State { final zoom = (context.findAncestorWidgetOfExactType()?.transformationController?.value.getMaxScaleOnAxis() ?? 1.0); final adjustedDelta = details.delta*zoom; offset += adjustedDelta; - widget.onDrag(widget.node.id, offset); + // Snap to grid + final snappedOffset = Offset( + (offset.dx / widget.gridSize).round() * widget.gridSize, + (offset.dy / widget.gridSize).round() * widget.gridSize, + ); + widget.onDrag(widget.node.id, snappedOffset); } }, onPanEnd: (_){ diff --git a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart index ad00f19f0..a04d80332 100644 --- a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart +++ b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart @@ -3,7 +3,6 @@ import 'package:apidash/screens/dashflow/dashflow_builder/nodes.dart'; import 'package:flutter/material.dart'; import 'package:apidash/providers/providers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter/widgets.dart'; class WorkflowCanvas extends ConsumerStatefulWidget { const WorkflowCanvas({super.key}); @@ -33,37 +32,47 @@ class _WorkflowCanvasState extends ConsumerState { } void _onTransformChanged() { - if (!_needsRepaint){ - _needsRepaint = true; - WidgetsBinding.instance.addPostFrameCallback((_){ - if (mounted){ - setState((){ - _needsRepaint = false; - }); - } + if (!_needsRepaint) { + _needsRepaint = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _needsRepaint = false; + }); + } + }); } - ); - } } @override Widget build(BuildContext context) { final mq = MediaQuery.of(context).size; - const canvasSize = 5000.00; + final canvasSize = 5000.00; + const double baseGridSize = 20; final nodes = ref.watch(workflowProvider); return Scaffold( appBar: AppBar(title: Text("New Dashflow")), body: Padding( - padding: const EdgeInsets.only(left: 5,right: 5,bottom: 10), + padding: const EdgeInsets.only(left: 5, right: 5, bottom: 10), child: Container( decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)), - color: const Color.fromARGB(255, 229, 247, 255), + gradient: LinearGradient( + colors: [ + Colors.black12, + Colors.transparent, + Colors.transparent, + Colors.transparent, + ], + begin: Alignment.topCenter, + end: Alignment.center, + ), ), child: InteractiveViewer( transformationController: _controller, constrained: false, - boundaryMargin: EdgeInsets.all(canvasSize/2), + boundaryMargin: EdgeInsets.all(canvasSize / 2), minScale: 0.5, maxScale: 3, child: SizedBox( @@ -71,6 +80,7 @@ class _WorkflowCanvasState extends ConsumerState { width: canvasSize, child: CustomPaint( painter: GridPainter( + baseGridSize: baseGridSize, canvasSize: canvasSize, transformation: _controller.value, viewportSize: mq, @@ -87,9 +97,11 @@ class _WorkflowCanvasState extends ConsumerState { left: centeredOffset.dx, top: centeredOffset.dy, child: DraggableNode( + gridSize: baseGridSize, node: node, - onDrag:(id, offset) => - ref.read(workflowProvider.notifier).updateNodeOffset(id, offset), + onDrag: (id, offset) => ref + .read(workflowProvider.notifier) + .updateNodeOffset(id, offset), ), ); }).toList(), @@ -102,4 +114,3 @@ class _WorkflowCanvasState extends ConsumerState { ); } } - diff --git a/pubspec.lock b/pubspec.lock index 7546654e1..4540793ca 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1080,14 +1080,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_drawing: - dependency: "direct main" - description: - name: path_drawing - sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 - url: "https://pub.dev" - source: hosted - version: "1.0.1" path_parsing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a64c7c40c..9ab7716dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,7 +70,6 @@ dependencies: url: https://github.com/google/flutter-desktop-embedding.git path: plugins/window_size carousel_slider: ^5.0.0 - path_drawing: ^1.0.1 vector_math: ^2.1.4 dependency_overrides: From e705a19f042e95e33714a68e6ea1d9c1b3d6cb1a Mon Sep 17 00:00:00 2001 From: HydrogenIITG Date: Sun, 20 Apr 2025 20:24:35 +0530 Subject: [PATCH 7/9] feat:added node connector --- lib/providers/ui_providers.dart | 2 +- .../dashflow_builder/node_connectors.dart | 80 +++++++++++++++++ .../dashflow/dashflow_builder/nodes.dart | 6 +- .../dashflow_builder/workflow_canvas.dart | 89 ++++++++++++------- 4 files changed, 140 insertions(+), 37 deletions(-) create mode 100644 lib/screens/dashflow/dashflow_builder/node_connectors.dart diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 573f318ae..ced3e35b4 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -40,7 +40,7 @@ final importFormatStateProvider = final userOnboardedProvider = StateProvider((ref) => false); final workflowProvider = StateNotifierProvider>((ref) => WorkflowNotifier()); - +final hoverNodeProvider = StateProvider((ref) => null); class WorkflowNotifier extends StateNotifier> { WorkflowNotifier() : super([ NodeData(id: 1, offset: Offset(0, 0)), diff --git a/lib/screens/dashflow/dashflow_builder/node_connectors.dart b/lib/screens/dashflow/dashflow_builder/node_connectors.dart new file mode 100644 index 000000000..c7a9ffa7d --- /dev/null +++ b/lib/screens/dashflow/dashflow_builder/node_connectors.dart @@ -0,0 +1,80 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'nodes.dart'; + +class ArrowPainter extends CustomPainter { + final List nodes; + final double gridSize; + final double canvasSize; + final int? hoveredNodeId; // Properly declared as optional + + ArrowPainter({ + required this.nodes, + required this.gridSize, + required this.canvasSize, + this.hoveredNodeId, + }); + + @override + void paint(Canvas canvas, Size size) { + if (nodes.length < 2) return; + + // Find Node 1 and Node 2 + final sourceNode = nodes.firstWhere((n) => n.id == 1); + final targetNode = nodes.firstWhere((n) => n.id == 2); + final isHighlighted = hoveredNodeId == 1 || hoveredNodeId == 2; + + // Define paint style + final paint = Paint() + ..color = isHighlighted ? Colors.blue : Colors.grey + ..style = PaintingStyle.stroke + ..strokeWidth = isHighlighted ? 3 : 2; + + // Get rendered sizes using GlobalKey + final sourceRenderBox = + sourceNode.sizeKey.currentContext?.findRenderObject() as RenderBox?; + final targetRenderBox = + targetNode.sizeKey.currentContext?.findRenderObject() as RenderBox?; + if (sourceRenderBox == null || targetRenderBox == null) return; + + final sourceSize = sourceRenderBox.size; + final targetSize = targetRenderBox.size; + + // Define flexible connection points (edges of cards) + final sourceEdgeX = + sourceNode.offset.dx + canvasSize / 2 + sourceSize.width / 2; + final sourceEdgeY = + sourceNode.offset.dy + canvasSize / 2 + sourceSize.height / 2; + final targetEdgeX = + targetNode.offset.dx + canvasSize / 2 + targetSize.width / 2; + final targetEdgeY = + targetNode.offset.dy + canvasSize / 2 + targetSize.height / 2; + + // Z-shaped path with two L-turns, snapped to grid + final midX = ((sourceEdgeX + targetEdgeX) / 2); + final pathPoints = [ + Offset(sourceEdgeX, sourceEdgeY), // Right edge of source node + Offset(midX, sourceEdgeY), // Horizontal to midpoint + Offset(midX, targetEdgeY), // Vertical to target height + Offset(targetEdgeX, targetEdgeY), // Left edge of target node + ]; + + // Draw the path + final path = Path() + ..moveTo(pathPoints[0].dx, pathPoints[0].dy) + ..lineTo(pathPoints[1].dx, pathPoints[1].dy) + ..lineTo(pathPoints[2].dx, pathPoints[2].dy) + ..lineTo(pathPoints[3].dx, pathPoints[3].dy); + canvas.drawPath(path, paint); + + // Debug prints (optional, remove after testing) + // print('Snapped Source: $snappedSource'); + // print('Middle Point: ${pathPoints[1]}'); + // print('Snapped Target: $snappedTarget'); + // print('Angle: $angle'); + } + + @override + bool shouldRepaint(ArrowPainter oldDelegate) => + nodes != oldDelegate.nodes || hoveredNodeId != oldDelegate.hoveredNodeId; +} diff --git a/lib/screens/dashflow/dashflow_builder/nodes.dart b/lib/screens/dashflow/dashflow_builder/nodes.dart index 937aaeb75..44a8ed5aa 100644 --- a/lib/screens/dashflow/dashflow_builder/nodes.dart +++ b/lib/screens/dashflow/dashflow_builder/nodes.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; class NodeData { final int id; final Offset offset; + final GlobalKey sizeKey = GlobalKey(); NodeData({required this.id, required this.offset}); @@ -58,12 +59,13 @@ class _DraggableNodeState extends State { }); }, child: Card( - elevation: 4, + key: widget.node.sizeKey, + elevation: isDragging ? 16 : 4, color: Colors.lightBlue[100], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(16.0), - child: Text("Node ${widget.node.id}", style: Theme.of(context).textTheme.bodyMedium), + child: Text("Node ${widget.node.id}, this is the test node", style: Theme.of(context).textTheme.bodyMedium), ), ), ); diff --git a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart index a04d80332..3fa67397d 100644 --- a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart +++ b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart @@ -1,4 +1,5 @@ import 'package:apidash/screens/dashflow/dashflow_builder/grid.dart'; +import 'package:apidash/screens/dashflow/dashflow_builder/node_connectors.dart'; import 'package:apidash/screens/dashflow/dashflow_builder/nodes.dart'; import 'package:flutter/material.dart'; import 'package:apidash/providers/providers.dart'; @@ -48,22 +49,22 @@ class _WorkflowCanvasState extends ConsumerState { Widget build(BuildContext context) { final mq = MediaQuery.of(context).size; final canvasSize = 5000.00; - const double baseGridSize = 20; + const double baseGridSize = 25; final nodes = ref.watch(workflowProvider); + final hoveredNodeId = ref.watch(hoverNodeProvider); + return Scaffold( - appBar: AppBar(title: Text("New Dashflow")), + appBar: AppBar(title: Text("Dashflow 1")), body: Padding( padding: const EdgeInsets.only(left: 5, right: 5, bottom: 10), child: Container( decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)), + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.all(Radius.circular(10)), gradient: LinearGradient( colors: [ Colors.black12, Colors.transparent, - Colors.transparent, - Colors.transparent, ], begin: Alignment.topCenter, end: Alignment.center, @@ -78,34 +79,54 @@ class _WorkflowCanvasState extends ConsumerState { child: SizedBox( height: canvasSize, width: canvasSize, - child: CustomPaint( - painter: GridPainter( - baseGridSize: baseGridSize, - canvasSize: canvasSize, - transformation: _controller.value, - viewportSize: mq, - ), - child: Stack( - clipBehavior: Clip.none, - children: nodes.map((node) { - // Translate node positions to center of canvas - final centeredOffset = Offset( - node.offset.dx + canvasSize / 2, - node.offset.dy + canvasSize / 2, - ); - return Positioned( - left: centeredOffset.dx, - top: centeredOffset.dy, - child: DraggableNode( - gridSize: baseGridSize, - node: node, - onDrag: (id, offset) => ref - .read(workflowProvider.notifier) - .updateNodeOffset(id, offset), - ), - ); - }).toList(), - ), + child: Stack( + children: [ + CustomPaint( + painter: ArrowPainter( + nodes: nodes, + canvasSize: canvasSize, + gridSize: baseGridSize, + hoveredNodeId: hoveredNodeId, // Pass the hovered node ID + ), + ), + CustomPaint( + painter: GridPainter( + baseGridSize: baseGridSize, + canvasSize: canvasSize, + transformation: _controller.value, + viewportSize: mq, + ), + child: Stack( + clipBehavior: Clip.none, + children: nodes.map((node) { + // Translate node positions to center of canvas + final centeredOffset = Offset( + node.offset.dx + canvasSize / 2, + node.offset.dy + canvasSize / 2, + ); + return Positioned( + left: centeredOffset.dx, + top: centeredOffset.dy, + child: MouseRegion( + onEnter: (_) => ref + .read(hoverNodeProvider.notifier) + .state = node.id, + onExit: (_) => ref + .read(hoverNodeProvider.notifier) + .state = null, + child: DraggableNode( + gridSize: baseGridSize, + node: node, + onDrag: (id, offset) => ref + .read(workflowProvider.notifier) + .updateNodeOffset(id, offset), + ), + ), + ); + }).toList(), + ), + ), + ], ), ), ), From 0cfffb2df8bda73ab5c0d8cc08b2f971b3958143 Mon Sep 17 00:00:00 2001 From: HydrogenIITG Date: Sat, 26 Apr 2025 23:32:20 +0530 Subject: [PATCH 8/9] feat:added control node --- .../dashflow_builder/node_connectors.dart | 1 - .../dashflow/dashflow_builder/nodes.dart | 150 ++++++++++++++++++ .../dashflow_builder/workflow_canvas.dart | 124 ++++++++------- 3 files changed, 219 insertions(+), 56 deletions(-) diff --git a/lib/screens/dashflow/dashflow_builder/node_connectors.dart b/lib/screens/dashflow/dashflow_builder/node_connectors.dart index c7a9ffa7d..c239d94f2 100644 --- a/lib/screens/dashflow/dashflow_builder/node_connectors.dart +++ b/lib/screens/dashflow/dashflow_builder/node_connectors.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'package:flutter/material.dart'; import 'nodes.dart'; diff --git a/lib/screens/dashflow/dashflow_builder/nodes.dart b/lib/screens/dashflow/dashflow_builder/nodes.dart index 44a8ed5aa..5a8e1fa1c 100644 --- a/lib/screens/dashflow/dashflow_builder/nodes.dart +++ b/lib/screens/dashflow/dashflow_builder/nodes.dart @@ -71,3 +71,153 @@ class _DraggableNodeState extends State { ); } } + +class ControlNode extends StatefulWidget { + final Offset offset; + final Function(Offset newOffset) onDrag; + final double gridSize; + + const ControlNode({ + super.key, + required this.offset, + required this.onDrag, + required this.gridSize, + }); + + @override + State createState() => _ControlNodeState(); +} + +class _ControlNodeState extends State { + late Offset offset; + bool isDragging = false; + + void _runFlow(BuildContext context) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Run Flow'), + content: const Text('Workflow executed successfully (mock).'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } + + void _addAnnotation(BuildContext context) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Add Annotation'), + content: const TextField(decoration: InputDecoration(hintText: 'Enter note')), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Save'), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ], + ), + ); + } + + void _clearCanvas(BuildContext context) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Clear Canvas'), + content: const Text('This will reset all nodes. Proceed?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Clear'), + ), + ], + ), + ); + } + + @override + void initState() { + super.initState(); + offset = widget.offset; + } + + @override + Widget build(BuildContext context) { + return Positioned( + left: offset.dx, + top: offset.dy, + child: GestureDetector( + onPanUpdate: (details) { + setState(() { + offset += details.delta; + final snappedOffset = Offset( + (offset.dx / widget.gridSize).round() * widget.gridSize, + (offset.dy / widget.gridSize).round() * widget.gridSize, + ); + widget.onDrag(snappedOffset); + }); + }, + child: Card( + elevation: isDragging ? 8 : 4, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.secondaryContainer, + Theme.of(context).colorScheme.secondary, + ], + ), + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Controls', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.play_arrow, size: 20), + onPressed: () => _runFlow(context), + tooltip: 'Run Flow', + ), + IconButton( + icon: const Icon(Icons.note_add, size: 20), + onPressed: () => _addAnnotation(context), + tooltip: 'Add Annotation', + ), + IconButton( + icon: const Icon(Icons.delete, size: 20), + onPressed: () => _clearCanvas(context), + tooltip: 'Clear Canvas', + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart index 3fa67397d..354d9227f 100644 --- a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart +++ b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart @@ -15,6 +15,7 @@ class WorkflowCanvas extends ConsumerStatefulWidget { class _WorkflowCanvasState extends ConsumerState { final TransformationController _controller = TransformationController(); bool _needsRepaint = false; + Offset _controlNodeOffset = const Offset(100, 100); // Initial position @override void initState() { @@ -70,66 +71,79 @@ class _WorkflowCanvasState extends ConsumerState { end: Alignment.center, ), ), - child: InteractiveViewer( - transformationController: _controller, - constrained: false, - boundaryMargin: EdgeInsets.all(canvasSize / 2), - minScale: 0.5, - maxScale: 3, - child: SizedBox( - height: canvasSize, - width: canvasSize, - child: Stack( - children: [ - CustomPaint( - painter: ArrowPainter( - nodes: nodes, - canvasSize: canvasSize, - gridSize: baseGridSize, - hoveredNodeId: hoveredNodeId, // Pass the hovered node ID + child: Stack(children: [ + InteractiveViewer( + transformationController: _controller, + constrained: false, + boundaryMargin: EdgeInsets.all(canvasSize / 2), + minScale: 0.5, + maxScale: 3, + child: SizedBox( + height: canvasSize, + width: canvasSize, + child: Stack( + children: [ + CustomPaint( + painter: ArrowPainter( + nodes: nodes, + canvasSize: canvasSize, + gridSize: baseGridSize, + hoveredNodeId: + hoveredNodeId, // Pass the hovered node ID + ), ), - ), - CustomPaint( - painter: GridPainter( - baseGridSize: baseGridSize, - canvasSize: canvasSize, - transformation: _controller.value, - viewportSize: mq, - ), - child: Stack( - clipBehavior: Clip.none, - children: nodes.map((node) { - // Translate node positions to center of canvas - final centeredOffset = Offset( - node.offset.dx + canvasSize / 2, - node.offset.dy + canvasSize / 2, - ); - return Positioned( - left: centeredOffset.dx, - top: centeredOffset.dy, - child: MouseRegion( - onEnter: (_) => ref - .read(hoverNodeProvider.notifier) - .state = node.id, - onExit: (_) => ref - .read(hoverNodeProvider.notifier) - .state = null, - child: DraggableNode( - gridSize: baseGridSize, - node: node, - onDrag: (id, offset) => ref - .read(workflowProvider.notifier) - .updateNodeOffset(id, offset), + CustomPaint( + painter: GridPainter( + baseGridSize: baseGridSize, + canvasSize: canvasSize, + transformation: _controller.value, + viewportSize: mq, + ), + child: Stack( + clipBehavior: Clip.none, + children: nodes.map((node) { + // Translate node positions to center of canvas + final centeredOffset = Offset( + node.offset.dx + canvasSize / 2, + node.offset.dy + canvasSize / 2, + ); + return Positioned( + left: centeredOffset.dx, + top: centeredOffset.dy, + child: MouseRegion( + onEnter: (_) => ref + .read(hoverNodeProvider.notifier) + .state = node.id, + onExit: (_) => ref + .read(hoverNodeProvider.notifier) + .state = null, + child: DraggableNode( + gridSize: baseGridSize, + node: node, + onDrag: (id, offset) => ref + .read(workflowProvider.notifier) + .updateNodeOffset(id, offset), + ), ), - ), - ); - }).toList(), + ); + }).toList(), + ), ), - ), - ], + ], + ), ), ), - ), + // Control node, always visible and draggable + ControlNode( + offset: _controlNodeOffset, + onDrag: (newOffset) { + setState(() { + _controlNodeOffset = newOffset; + }); + }, + gridSize: baseGridSize, + ), + ]), ), ), ); From b327ea6d85bfeb4f0105409943d47e47ae96a4b5 Mon Sep 17 00:00:00 2001 From: HydrogenIITG Date: Sun, 27 Apr 2025 02:47:23 +0530 Subject: [PATCH 9/9] feat:added add node and node settings --- lib/providers/ui_providers.dart | 32 ++- .../dashflow_builder/node_connectors.dart | 12 +- .../dashflow/dashflow_builder/nodes.dart | 220 ++++++++++++------ .../dashflow_builder/workflow_canvas.dart | 3 + .../dashflow_builder/workflow_settings.dart | 85 +++++++ 5 files changed, 283 insertions(+), 69 deletions(-) create mode 100644 lib/screens/dashflow/dashflow_builder/workflow_settings.dart diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index ced3e35b4..1507b3ba1 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -41,16 +41,44 @@ final userOnboardedProvider = StateProvider((ref) => false); final workflowProvider = StateNotifierProvider>((ref) => WorkflowNotifier()); final hoverNodeProvider = StateProvider((ref) => null); +final connectionListProvider = StateProvider>((ref) => []); class WorkflowNotifier extends StateNotifier> { WorkflowNotifier() : super([ NodeData(id: 1, offset: Offset(0, 0)), NodeData(id: 2, offset: Offset(50, 50)), ]); - void updateNodeOffset(int id, Offset newOffset) { + void updateNodeOffset(int id, Offset newOffset) { state = [ for (final node in state) if (node.id == id) node.copyWith(offset: newOffset) else node, ]; } -} \ No newline at end of file + + void addNode(NodeData node) { + state = [...state, node]; + } + + void updateNode(int id, {String? title, String? url, Map? headers}) { + state = [ + for (final node in state) + if (node.id == id) + node.copyWith(title: title, url: url, headers: headers) + else + node, + ]; + } +} + +class ConnectionListNotifier extends StateNotifier> { + ConnectionListNotifier() : super([]); // Initialize with an empty list + + void addConnection(Connection connection) { + state = [...state, connection]; // Add a new connection + } + + void removeConnection(Connection connection) { + state = state.where((c) => c != connection).toList(); // Remove a connection + } +} + diff --git a/lib/screens/dashflow/dashflow_builder/node_connectors.dart b/lib/screens/dashflow/dashflow_builder/node_connectors.dart index c239d94f2..4952e9741 100644 --- a/lib/screens/dashflow/dashflow_builder/node_connectors.dart +++ b/lib/screens/dashflow/dashflow_builder/node_connectors.dart @@ -3,12 +3,14 @@ import 'nodes.dart'; class ArrowPainter extends CustomPainter { final List nodes; + final List connections; final double gridSize; final double canvasSize; final int? hoveredNodeId; // Properly declared as optional ArrowPainter({ required this.nodes, + required this.connections, required this.gridSize, required this.canvasSize, this.hoveredNodeId, @@ -29,6 +31,12 @@ class ArrowPainter extends CustomPainter { ..style = PaintingStyle.stroke ..strokeWidth = isHighlighted ? 3 : 2; + for (var conn in connections) { + final fromNode = nodes.firstWhere((n) => n.id == conn.from, orElse: () => NodeData(id: -1, offset: Offset.zero)); + final toNode = nodes.firstWhere((n) => n.id == conn.to, orElse: () => NodeData(id: -1, offset: Offset.zero)); + if (fromNode.id == -1 || toNode.id == -1) continue;} + + // Get rendered sizes using GlobalKey final sourceRenderBox = sourceNode.sizeKey.currentContext?.findRenderObject() as RenderBox?; @@ -75,5 +83,5 @@ class ArrowPainter extends CustomPainter { @override bool shouldRepaint(ArrowPainter oldDelegate) => - nodes != oldDelegate.nodes || hoveredNodeId != oldDelegate.hoveredNodeId; -} + true; +} \ No newline at end of file diff --git a/lib/screens/dashflow/dashflow_builder/nodes.dart b/lib/screens/dashflow/dashflow_builder/nodes.dart index 5a8e1fa1c..9266125dd 100644 --- a/lib/screens/dashflow/dashflow_builder/nodes.dart +++ b/lib/screens/dashflow/dashflow_builder/nodes.dart @@ -1,30 +1,58 @@ +import 'package:apidash/providers/ui_providers.dart'; +import 'package:apidash/screens/dashflow/dashflow_builder/workflow_settings.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; class NodeData { final int id; final Offset offset; final GlobalKey sizeKey = GlobalKey(); + final String title; // Added for settings + final String url; // Added for settings + final Map headers; // Added for settings - NodeData({required this.id, required this.offset}); - NodeData copyWith({Offset? offset}) => - NodeData(id: id, offset: offset ?? this.offset); + NodeData({required this.id, required this.offset, + GlobalKey? sizeKey,this.title = 'REST Call', + this.url = '', + this.headers = const {},}); + +NodeData copyWith({ + Offset? offset, + String? title, + String? url, + Map? headers, + }) { + return NodeData( + id: id, + offset: offset ?? this.offset, + title: title ?? this.title, + url: url ?? this.url, + headers: headers ?? this.headers, + ); + } } -class DraggableNode extends StatefulWidget { +class DraggableNode extends ConsumerStatefulWidget { final NodeData node; final Function(int id, Offset newOffset) onDrag; final double gridSize; // Added for snap-to-grid - const DraggableNode({super.key, required this.node, required this.onDrag,required this.gridSize,}); + const DraggableNode({ + super.key, + required this.node, + required this.onDrag, + required this.gridSize, + }); @override - State createState() => _DraggableNodeState(); + ConsumerState createState() => _DraggableNodeState(); } -class _DraggableNodeState extends State { +class _DraggableNodeState extends ConsumerState { late Offset offset; bool isDragging = false; + Offset? _dragStart; // Added for connection dragging @override void initState() { @@ -35,29 +63,69 @@ class _DraggableNodeState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onPanStart:(_){ - setState((){ - isDragging =true; + onPanStart: (_) { + setState(() { + isDragging = true; }); }, onPanUpdate: (details) { - if(isDragging){ - final zoom = (context.findAncestorWidgetOfExactType()?.transformationController?.value.getMaxScaleOnAxis() ?? 1.0); - final adjustedDelta = details.delta*zoom; - offset += adjustedDelta; - // Snap to grid + if (isDragging) { + final zoom = (context + .findAncestorWidgetOfExactType() + ?.transformationController + ?.value + .getMaxScaleOnAxis() ?? + 1.0); + final adjustedDelta = details.delta * zoom; + offset += adjustedDelta; + // Snap to grid final snappedOffset = Offset( (offset.dx / widget.gridSize).round() * widget.gridSize, (offset.dy / widget.gridSize).round() * widget.gridSize, ); - widget.onDrag(widget.node.id, snappedOffset); + widget.onDrag(widget.node.id, snappedOffset); } }, - onPanEnd: (_){ - setState((){ + onPanEnd: (_) { + setState(() { isDragging = false; }); }, + onTap: () { + showDialog( + context: context, + builder: (_) => SettingsDialog( + node: widget.node, + onSave: (title, url, headers) { + ref.read(workflowProvider.notifier).updateNode( + widget.node.id, + title: title, + url: url, + headers: headers, + ); + }, + ), + ); + }, + onLongPressStart: (details) { + _dragStart = details.localPosition; + }, + onLongPressEnd: (details) { + final nodes = ref.read(workflowProvider); + final endNode = nodes.firstWhere( + (n) => + (n.offset - (offset + details.localPosition)).distance < 50 && + n.id != widget.node.id, + orElse: () => widget.node, + ); + if (endNode != widget.node) { + ref.read(connectionListProvider.notifier).state = [ + ...ref.read(connectionListProvider), + Connection(from: widget.node.id, to: endNode.id), + ]; + } + _dragStart = null; + }, child: Card( key: widget.node.sizeKey, elevation: isDragging ? 16 : 4, @@ -65,14 +133,15 @@ class _DraggableNodeState extends State { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(16.0), - child: Text("Node ${widget.node.id}, this is the test node", style: Theme.of(context).textTheme.bodyMedium), + child: Text("Node ${widget.node.id}, this is the test node", + style: Theme.of(context).textTheme.bodyMedium), ), ), ); } } -class ControlNode extends StatefulWidget { +class ControlNode extends ConsumerStatefulWidget { final Offset offset; final Function(Offset newOffset) onDrag; final double gridSize; @@ -85,10 +154,10 @@ class ControlNode extends StatefulWidget { }); @override - State createState() => _ControlNodeState(); + ConsumerState createState() => _ControlNodeState(); } -class _ControlNodeState extends State { +class _ControlNodeState extends ConsumerState { late Offset offset; bool isDragging = false; @@ -113,7 +182,8 @@ class _ControlNodeState extends State { context: context, builder: (_) => AlertDialog( title: const Text('Add Annotation'), - content: const TextField(decoration: InputDecoration(hintText: 'Enter note')), + content: const TextField( + decoration: InputDecoration(hintText: 'Enter note')), actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -172,52 +242,72 @@ class _ControlNodeState extends State { }, child: Card( elevation: isDragging ? 8 : 4, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.secondaryContainer, - Theme.of(context).colorScheme.secondary, - ], - ), - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Controls', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 8), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.play_arrow, size: 20), - onPressed: () => _runFlow(context), - tooltip: 'Run Flow', - ), - IconButton( - icon: const Icon(Icons.note_add, size: 20), - onPressed: () => _addAnnotation(context), - tooltip: 'Add Annotation', - ), - IconButton( - icon: const Icon(Icons.delete, size: 20), - onPressed: () => _clearCanvas(context), - tooltip: 'Clear Canvas', - ), - ], - ), - ], - ), + color: Colors.white, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 15.0, vertical: 5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Icon(Icons.drag_indicator), + const SizedBox( + width: 10, + ), + IconButton( + onPressed: () { + ref.read(workflowProvider.notifier).addNode( + NodeData( + id: DateTime.now().millisecondsSinceEpoch, + offset: Offset( + (100 / widget.gridSize).round() * widget.gridSize, + (100 / widget.gridSize).round() * widget.gridSize, + ), + title: 'REST Call', + ), + ); + }, + icon: const Icon(Icons.add, size: 20), + tooltip: 'Add node', + ), + const SizedBox( + width: 10, + ), + ElevatedButton.icon( + label: const Text('Run'), + icon: const Icon(Icons.play_arrow, size: 20), + onPressed: () => _runFlow(context), + ), + const SizedBox( + width: 10, + ), + IconButton( + icon: const Icon(Icons.note_add, size: 20), + onPressed: () => _addAnnotation(context), + tooltip: 'Add Annotation', + ), + const SizedBox( + width: 10, + ), + IconButton( + icon: const Icon(Icons.delete, size: 20), + onPressed: () => _clearCanvas(context), + tooltip: 'Clear Canvas', + ), + ], ), ), ), ), ); } +} + +// Define Connection class for node connections +class Connection { + final int from; + final int to; + + Connection({required this.from, required this.to}); } \ No newline at end of file diff --git a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart index 354d9227f..49444378e 100644 --- a/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart +++ b/lib/screens/dashflow/dashflow_builder/workflow_canvas.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/providers/providers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + class WorkflowCanvas extends ConsumerStatefulWidget { const WorkflowCanvas({super.key}); @@ -53,6 +54,7 @@ class _WorkflowCanvasState extends ConsumerState { const double baseGridSize = 25; final nodes = ref.watch(workflowProvider); final hoveredNodeId = ref.watch(hoverNodeProvider); + final connections = ref.watch(connectionListProvider); return Scaffold( appBar: AppBar(title: Text("Dashflow 1")), @@ -86,6 +88,7 @@ class _WorkflowCanvasState extends ConsumerState { CustomPaint( painter: ArrowPainter( nodes: nodes, + connections: connections, canvasSize: canvasSize, gridSize: baseGridSize, hoveredNodeId: diff --git a/lib/screens/dashflow/dashflow_builder/workflow_settings.dart b/lib/screens/dashflow/dashflow_builder/workflow_settings.dart new file mode 100644 index 000000000..669841287 --- /dev/null +++ b/lib/screens/dashflow/dashflow_builder/workflow_settings.dart @@ -0,0 +1,85 @@ +import 'package:apidash/screens/dashflow/dashflow_builder/nodes.dart'; +import 'package:flutter/material.dart'; + +class SettingsDialog extends StatefulWidget { + final NodeData node; + final Function(String title, String url, Map headers) onSave; + + const SettingsDialog({super.key, required this.node, required this.onSave}); + + @override + State createState() => _SettingsDialogState(); +} + +class _SettingsDialogState extends State { + late TextEditingController _titleController; + late TextEditingController _urlController; + late TextEditingController _headersController; + + @override + void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.node.title); + _urlController = TextEditingController(text: widget.node.url); + _headersController = TextEditingController( + text: widget.node.headers.entries.map((e) => '${e.key}: ${e.value}').join('\n'), + ); + } + + @override + void dispose() { + _titleController.dispose(); + _urlController.dispose(); + _headersController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Node Settings'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _titleController, + decoration: const InputDecoration(labelText: 'Title'), + ), + TextField( + controller: _urlController, + decoration: const InputDecoration(labelText: 'URL'), + ), + TextField( + controller: _headersController, + decoration: const InputDecoration(labelText: 'Headers (key: value)'), + maxLines: 4, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + final headers = _headersController.text + .split('\n') + .fold>({}, (map, line) { + final parts = line.split(': '); + if (parts.length == 2) map[parts[0]] = parts[1]; + return map; + }); + widget.onSave( + _titleController.text, + _urlController.text, + headers, + ); + Navigator.pop(context); + }, + child: const Text('Save'), + ), + ], + ); + } +} \ No newline at end of file