Skip to content

Commit 6bb7545

Browse files
committed
feat: export image as png
1 parent 0625309 commit 6bb7545

File tree

2 files changed

+104
-32
lines changed

2 files changed

+104
-32
lines changed

lib/features/workspace/providers/workspace_provider.dart

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
// lib/features/workspace/providers/workspace_provider.dart (Fully Modified)
2-
3-
import 'dart:ui';
1+
import 'dart:ui' as ui;
42
import 'dart:convert'; // For jsonDecode/jsonEncode
53
import 'dart:math';
64
import 'package:cookethflow/core/helpers/file_helper.dart';
@@ -11,6 +9,7 @@ import 'package:cookethflow/core/utils/enums.dart';
119
import 'package:cookethflow/core/utils/state_handler.dart';
1210
import 'package:cookethflow/features/dashboard/providers/dashboard_provider.dart';
1311
import 'package:cookethflow/features/models/canvas_models/canvas_object.dart';
12+
import 'package:cookethflow/features/models/canvas_models/canvas_painter.dart';
1413
import 'package:cookethflow/features/models/canvas_models/objects/circle_object.dart';
1514
import 'package:cookethflow/features/models/canvas_models/objects/connector_object.dart';
1615
import 'package:cookethflow/features/models/canvas_models/objects/cylinder_object.dart';
@@ -78,8 +77,7 @@ class WorkspaceProvider extends StateHandler {
7877
String? _connectorSourceId;
7978
Alignment? _connectorSourceAlignment;
8079
Offset? _connectorDragPosition;
81-
82-
// Create an instance of FileServices
80+
8381
final FileServices _fileServices = FileServices();
8482

8583
bool get isLoading => _isLoading;
@@ -118,40 +116,34 @@ class WorkspaceProvider extends StateHandler {
118116
return _tempQuillController;
119117
}
120118

121-
// --- NEW EXPORT METHOD ---
122119
Future<void> exportWorkspaceAsJson() async {
123120
if (_currentWorkspace == null) {
124121
print("Cannot export: No workspace is currently loaded.");
125122
return;
126123
}
127124

128125
try {
129-
// 1. Gather all necessary data
130126
final workspaceData = _currentWorkspace!.toJson();
131127
final canvasObjectsData =
132128
_canvasObjects.values.map((obj) => obj.toJson()).toList();
133129

134-
// 2. Structure the data into a single map
135130
final exportData = {
136131
'workspace': {
137132
'name': workspaceData["name"],
138-
'data': workspaceData["data"]
133+
'data': workspaceData["data"],
139134
},
140135
'canvasObjects': canvasObjectsData,
141136
};
142137

143-
// 3. Encode the map into a formatted JSON string
144-
const jsonEncoder = JsonEncoder.withIndent(' '); // For pretty printing
138+
const jsonEncoder = JsonEncoder.withIndent(' ');
145139
final jsonString = jsonEncoder.convert(exportData);
146140

147-
// 4. Generate a safe and unique file name
148141
final safeWorkspaceName = _currentWorkspace!.name
149142
.replaceAll(RegExp(r'[^\w\s-]'), '')
150143
.replaceAll(' ', '_');
151144
final uniqueId = const Uuid().v4().substring(0, 8);
152-
final fileName = '${safeWorkspaceName}_$uniqueId'; // Service adds extension
145+
final fileName = '${safeWorkspaceName}_$uniqueId';
153146

154-
// 5. Use the FileServices to trigger the download
155147
final result = await _fileServices.exportFile(
156148
defaultName: fileName,
157149
jsonString: jsonString,
@@ -167,6 +159,87 @@ class WorkspaceProvider extends StateHandler {
167159
}
168160
}
169161

162+
Future<void> exportWorkspaceAsPng() async {
163+
if (_currentWorkspace == null || _canvasObjects.isEmpty) {
164+
print("Cannot export: No workspace or content to export.");
165+
return;
166+
}
167+
try {
168+
Rect? contentBounds;
169+
for (final object in _canvasObjects.values) {
170+
if (contentBounds == null) {
171+
contentBounds = object.getBounds();
172+
} else {
173+
contentBounds = contentBounds.expandToInclude(object.getBounds());
174+
}
175+
}
176+
177+
if (contentBounds == null) {
178+
print("No objects on canvas to export.");
179+
return;
180+
}
181+
182+
const double padding = 50.0;
183+
final imageBounds = Rect.fromLTRB(
184+
contentBounds.left - padding,
185+
contentBounds.top - padding,
186+
contentBounds.right + padding,
187+
contentBounds.bottom + padding,
188+
);
189+
190+
final recorder = ui.PictureRecorder();
191+
final canvas = Canvas(recorder);
192+
193+
canvas.translate(-imageBounds.left, -imageBounds.top);
194+
195+
final backgroundPaint = Paint()..color = _currentWorkspaceColor;
196+
canvas.drawRect(
197+
Rect.fromLTWH(0, 0, imageBounds.width, imageBounds.height),
198+
backgroundPaint);
199+
200+
final painter = CanvasPainter(
201+
canvasObjects: _canvasObjects,
202+
userCursors: {},
203+
currentlySelectedObjectId: null,
204+
interactionMode: InteractionMode.none,
205+
handleRadius: 0,
206+
connectionPointRadius: _connectionPointRadius,
207+
connectorDragPosition: null,
208+
connectorSourceId: null,
209+
connectorSourceAlignment: null,
210+
);
211+
painter.paint(canvas, imageBounds.size);
212+
213+
final picture = recorder.endRecording();
214+
final img = await picture.toImage(
215+
imageBounds.width.toInt(),
216+
imageBounds.height.toInt(),
217+
);
218+
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
219+
final pngBytes = byteData!.buffer.asUint8List();
220+
221+
final safeWorkspaceName = _currentWorkspace!.name
222+
.replaceAll(RegExp(r'[^\w\s-]'), '')
223+
.replaceAll(' ', '_');
224+
final uniqueId = const Uuid().v4().substring(0, 8);
225+
final fileName = '${safeWorkspaceName}_$uniqueId';
226+
227+
final result = await _fileServices.exportPNG(
228+
defaultName: fileName,
229+
pngBytes: pngBytes,
230+
);
231+
232+
if (result == 'success') {
233+
print("Workspace exported successfully as $fileName.png");
234+
} else {
235+
print("PNG export failed or was cancelled: $result");
236+
}
237+
} catch (e) {
238+
print("An error occurred during PNG export: $e");
239+
}
240+
}
241+
242+
170243
void setStickyNoteMode(Color color) {
171244
_currentMode = DrawMode.stickyNote;
172245
_nextObjectColor = color;
@@ -279,7 +352,7 @@ class WorkspaceProvider extends StateHandler {
279352
}
280353
}
281354
}
282-
355+
283356
if (payload['deleted_ids'] != null) {
284357
final List<String> deletedIds = List<String>.from(
285358
payload['deleted_ids'],
@@ -1000,4 +1073,5 @@ class WorkspaceProvider extends StateHandler {
10001073
return a == b;
10011074
}
10021075
}
1003-
}
1076+
}
1077+

lib/features/workspace/widgets/export_dialog.dart

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
// lib/features/workspace/widgets/export_dialog.dart (Fully Modified)
2-
31
import 'package:cookethflow/core/providers/supabase_provider.dart';
2+
import 'package:cookethflow/core/theme/colors.dart';
43
import 'package:cookethflow/features/workspace/providers/workspace_provider.dart';
54
import 'package:flutter/material.dart';
65
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -9,13 +8,9 @@ import 'package:cookethflow/core/helpers/responsive_layout.helper.dart' as rh;
98

109
class ExportDialog extends StatefulWidget {
1110
final SupabaseService su;
12-
final WorkspaceProvider wp; // MODIFIED: Added WorkspaceProvider
11+
final WorkspaceProvider wp;
1312

14-
const ExportDialog({
15-
super.key,
16-
required this.su,
17-
required this.wp, // MODIFIED: Added to constructor
18-
});
13+
const ExportDialog({super.key, required this.su, required this.wp});
1914

2015
@override
2116
State<ExportDialog> createState() => _ExportDialogState();
@@ -36,7 +31,9 @@ class _ExportDialogState extends State<ExportDialog> {
3631
padding: EdgeInsets.all(24.r),
3732
decoration: BoxDecoration(
3833
color:
39-
widget.su.isDark ? Color.fromRGBO(48, 48, 48, 1) : Colors.white,
34+
widget.su.isDark
35+
? const Color.fromRGBO(48, 48, 48, 1)
36+
: Colors.white,
4037
borderRadius: BorderRadius.circular(16.r),
4138
boxShadow: [
4239
BoxShadow(
@@ -87,7 +84,6 @@ class _ExportDialogState extends State<ExportDialog> {
8784
],
8885
),
8986
SizedBox(height: 24.h),
90-
9187
Row(
9288
mainAxisAlignment: MainAxisAlignment.spaceBetween,
9389
children: [
@@ -154,20 +150,22 @@ class _ExportDialogState extends State<ExportDialog> {
154150
],
155151
),
156152
SizedBox(height: 32.h),
157-
158153
SizedBox(
159154
width: double.infinity,
160155
child: ElevatedButton(
161156
onPressed: () {
162-
// MODIFIED: Implement export logic
163157
if (_selectedFormat == 'JSON') {
164158
widget.wp.exportWorkspaceAsJson();
165159
} else if (_selectedFormat == 'PNG') {
166-
// TODO: Implement PNG export logic
167-
print('Exporting as PNG is not implemented yet.');
160+
widget.wp.exportWorkspaceAsPng();
168161
} else if (_selectedFormat == 'SVG') {
169-
// TODO: Implement SVG export logic
170162
print('Exporting as SVG is not implemented yet.');
163+
ScaffoldMessenger.of(context).showSnackBar(
164+
SnackBar(
165+
content: Text('Coming soon!'),
166+
backgroundColor: primaryColor,
167+
),
168+
);
171169
}
172170
Navigator.of(context).pop();
173171
},
@@ -199,4 +197,4 @@ class _ExportDialogState extends State<ExportDialog> {
199197
),
200198
);
201199
}
202-
}
200+
}

0 commit comments

Comments
 (0)