Skip to content

Commit 0625309

Browse files
committed
feat: workspace export as json
1 parent 9ecfb86 commit 0625309

File tree

10 files changed

+422
-51
lines changed

10 files changed

+422
-51
lines changed

lib/core/helpers/file_helper.dart

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import 'dart:convert';
2+
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
3+
import 'package:cookethflow/core/helpers/platform_file_helper.dart';
4+
import 'package:flutter/foundation.dart';
5+
import 'package:universal_html/html.dart' as html;
6+
7+
class FileServices {
8+
final FileSelectorPlatform fileSelector = FileSelectorPlatform.instance;
9+
10+
Future<String> exportFile({
11+
required String defaultName,
12+
required String jsonString,
13+
}) async {
14+
try {
15+
// Ensure the default name ends with .json
16+
final sanitizedName = defaultName.toLowerCase().endsWith('.json')
17+
? defaultName
18+
: '$defaultName.json';
19+
20+
if (kIsWeb) {
21+
// Web: Use browser download API
22+
final bytes = Uint8List.fromList(utf8.encode(jsonString));
23+
final blob = html.Blob([bytes], 'application/json');
24+
final url = html.Url.createObjectUrlFromBlob(blob);
25+
final anchor = html.AnchorElement(href: url)
26+
..setAttribute('download', sanitizedName)
27+
..click();
28+
html.Url.revokeObjectUrl(url);
29+
return 'success';
30+
} else {
31+
// Desktop: Use file_selector
32+
const XTypeGroup typeGroup = XTypeGroup(
33+
label: 'JSON',
34+
extensions: ['json'],
35+
mimeTypes: ['application/json'],
36+
);
37+
38+
final String? path = await fileSelector.getSavePath(
39+
acceptedTypeGroups: [typeGroup],
40+
suggestedName: sanitizedName,
41+
);
42+
43+
if (path == null) {
44+
return 'Save operation cancelled';
45+
}
46+
47+
// Ensure the selected path ends with .json
48+
final String savePath =
49+
path.toLowerCase().endsWith('.json') ? path : '$path.json';
50+
51+
final XFile file = XFile.fromData(
52+
Uint8List.fromList(utf8.encode(jsonString)),
53+
mimeType: 'application/json',
54+
name: savePath.split('/').last,
55+
);
56+
57+
await file.saveTo(savePath);
58+
return 'success';
59+
}
60+
} catch (e) {
61+
return e.toString();
62+
}
63+
}
64+
65+
Future<String> exportPNG({
66+
required String defaultName,
67+
required Uint8List pngBytes,
68+
}) async {
69+
try {
70+
// Ensure the default name ends with .png
71+
final sanitizedName = defaultName.toLowerCase().endsWith('.png')
72+
? defaultName
73+
: '$defaultName.png';
74+
75+
if (kIsWeb) {
76+
// Web: Use browser download API
77+
final blob = html.Blob([pngBytes], 'image/png');
78+
final url = html.Url.createObjectUrlFromBlob(blob);
79+
final anchor = html.AnchorElement(href: url)
80+
..setAttribute('download', sanitizedName)
81+
..click();
82+
html.Url.revokeObjectUrl(url);
83+
return 'success';
84+
} else {
85+
// Desktop: Use file_selector
86+
const XTypeGroup typeGroup = XTypeGroup(
87+
label: 'PNG Images',
88+
extensions: ['png'],
89+
mimeTypes: ['image/png'],
90+
);
91+
92+
final String? path = await fileSelector.getSavePath(
93+
acceptedTypeGroups: [typeGroup],
94+
suggestedName: sanitizedName,
95+
);
96+
97+
if (path == null) {
98+
return 'Save operation cancelled';
99+
}
100+
101+
// Ensure the selected path ends with .png
102+
final String savePath =
103+
path.toLowerCase().endsWith('.png') ? path : '$path.png';
104+
105+
final XFile file = XFile.fromData(
106+
pngBytes,
107+
mimeType: 'image/png',
108+
name: savePath.split('/').last,
109+
);
110+
111+
await file.saveTo(savePath);
112+
return 'success';
113+
}
114+
} catch (e) {
115+
return e.toString();
116+
}
117+
}
118+
119+
Future<String> exportSVG({
120+
required String defaultName,
121+
required String svgString,
122+
}) async {
123+
try {
124+
// Ensure the default name ends with .svg
125+
final sanitizedName = defaultName.toLowerCase().endsWith('.svg')
126+
? defaultName
127+
: '$defaultName.svg';
128+
129+
if (kIsWeb) {
130+
// Web: Use browser download API
131+
final bytes = Uint8List.fromList(utf8.encode(svgString));
132+
final blob = html.Blob([bytes], 'image/svg+xml');
133+
final url = html.Url.createObjectUrlFromBlob(blob);
134+
final anchor = html.AnchorElement(href: url)
135+
..setAttribute('download', sanitizedName)
136+
..click();
137+
html.Url.revokeObjectUrl(url);
138+
return 'success';
139+
} else {
140+
// Desktop: Use file_selector
141+
const XTypeGroup typeGroup = XTypeGroup(
142+
label: 'SVG Images',
143+
extensions: ['svg'],
144+
mimeTypes: ['image/svg+xml'],
145+
);
146+
147+
final String? path = await fileSelector.getSavePath(
148+
acceptedTypeGroups: [typeGroup],
149+
suggestedName: sanitizedName,
150+
);
151+
152+
if (path == null) {
153+
return 'Save operation cancelled';
154+
}
155+
156+
// Ensure the selected path ends with .svg
157+
final String savePath =
158+
path.toLowerCase().endsWith('.svg') ? path : '$path.svg';
159+
160+
final XFile file = XFile.fromData(
161+
Uint8List.fromList(utf8.encode(svgString)),
162+
mimeType: 'image/svg+xml',
163+
name: savePath.split('/').last,
164+
);
165+
166+
await file.saveTo(savePath);
167+
return 'success';
168+
}
169+
} catch (e) {
170+
return e.toString();
171+
}
172+
}
173+
174+
// For importing files
175+
Future<Map<String, dynamic>?> importJsonFile(Uint8List fileData) async {
176+
return await PlatformFileService.parseJSONFile(fileData);
177+
}
178+
179+
Future<Map<String, dynamic>?> pickAndReadJsonFile() async {
180+
return await PlatformFileService.pickJSONFile();
181+
}
182+
183+
Future<XFile?> selectImages() async {
184+
const XTypeGroup typeGroup = XTypeGroup(
185+
label: 'Images',
186+
mimeTypes: ['image/*'],
187+
);
188+
189+
try {
190+
final XFile? file =
191+
await fileSelector.openFile(acceptedTypeGroups: [typeGroup]);
192+
193+
if (file != null) {
194+
print('Selected file: ${file.path}');
195+
}
196+
return file;
197+
} catch (e) {
198+
print('Error selecting file: $e');
199+
return null;
200+
}
201+
}
202+
203+
Future<XFile?> importJsonFiles() async {
204+
const XTypeGroup typeGroup = XTypeGroup(
205+
label: 'JSON',
206+
mimeTypes: ['application/json'],
207+
extensions: ['json'],
208+
);
209+
210+
try {
211+
final XFile? file =
212+
await fileSelector.openFile(acceptedTypeGroups: [typeGroup]);
213+
if (file != null) {
214+
print('Selected file: ${file.path}');
215+
}
216+
return file;
217+
} catch (e) {
218+
print('Error in selecting file: $e');
219+
return null;
220+
}
221+
}
222+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter_document_picker/flutter_document_picker.dart';
5+
import 'package:universal_html/html.dart' as html;
6+
7+
class PlatformFileService {
8+
static Future<Map<String, dynamic>?> pickJSONFile() async {
9+
try {
10+
if (kIsWeb) {
11+
return await _pickFileWeb();
12+
} else {
13+
return await _pickFileNative();
14+
}
15+
} catch (e) {
16+
print("Error picking file: $e");
17+
return null;
18+
}
19+
}
20+
21+
static Future<Map<String, dynamic>?> _pickFileNative() async {
22+
try {
23+
final path = await FlutterDocumentPicker.openDocument(
24+
params: FlutterDocumentPickerParams(
25+
allowedFileExtensions: ['json'],
26+
invalidFileNameSymbols: ['/'],
27+
),
28+
);
29+
30+
if (path == null) return null;
31+
32+
final fileName = path.split('/').last;
33+
final File file = File(path);
34+
final bytes = await file.readAsBytes();
35+
36+
return {
37+
'name': fileName,
38+
'bytes': bytes,
39+
};
40+
} catch (e) {
41+
print("Native file picking error: $e");
42+
return null;
43+
}
44+
}
45+
46+
static Future<Map<String, dynamic>?> _pickFileWeb() async {
47+
try {
48+
final input = html.FileUploadInputElement()..accept = '.json';
49+
input.click();
50+
51+
await input.onChange.first;
52+
final files = input.files;
53+
if (files == null || files.isEmpty) return null;
54+
55+
final file = files[0];
56+
final reader = html.FileReader();
57+
reader.readAsArrayBuffer(file);
58+
59+
await reader.onLoad.first;
60+
final bytes = Uint8List.fromList(reader.result as List<int>);
61+
final fileName = file.name;
62+
63+
return {
64+
'name': fileName,
65+
'bytes': bytes,
66+
};
67+
} catch (e) {
68+
print("Web file picking error: $e");
69+
return null;
70+
}
71+
}
72+
73+
static Future<Map<String, dynamic>?> parseJSONFile(Uint8List bytes) async {
74+
try {
75+
final String content = utf8.decode(bytes);
76+
final Map<String, dynamic> jsonData = jsonDecode(content);
77+
return jsonData;
78+
} catch (e) {
79+
print("Error parsing JSON: $e");
80+
return null;
81+
}
82+
}
83+
}

lib/features/workspace/pages/desktop/workspace_desktop.dart

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// lib/features/workspace/pages/workspace_desktop.dart (Fully Modified)
2+
13
import 'package:cookethflow/core/helpers/responsive_layout.helper.dart' as rh;
24
import 'package:cookethflow/core/providers/supabase_provider.dart';
35
import 'package:cookethflow/features/workspace/pages/canvas_page.dart';
@@ -30,14 +32,15 @@ class WorkspaceDesktop extends StatelessWidget {
3032
body: Padding(
3133
padding: EdgeInsets.symmetric(horizontal: 40.w, vertical: 40.h),
3234
child: Stack(
33-
clipBehavior: Clip.none, // Allow toolbox to render outside the Stack's bounds
35+
clipBehavior: Clip.none,
3436
children: [
3537
const CanvasPage(),
3638

3739
const WorkspaceDrawer(),
3840
SizedBox(width: 20.w),
3941
Positioned(top: 0, left: 0.21.sw, child: UndoRedoButton(su: suprovider,)),
40-
Positioned(top: 0, right: 0.001.sw, child: ExportProjectButton(su: suprovider,)),
42+
// MODIFIED: Pass the WorkspaceProvider instance to the button
43+
Positioned(top: 0, right: 0.001.sw, child: ExportProjectButton(su: suprovider, wp: provider)),
4144

4245
Positioned(right: 0, top: 0.10.sh, child: ToolBar()),
4346

@@ -47,10 +50,8 @@ class WorkspaceDesktop extends StatelessWidget {
4750
child: ZoomControlButton(),
4851
),
4952

50-
// The new Object Editing Toolbox, positioned dynamically
5153
Consumer2<WorkspaceProvider, CanvasProvider>(
5254
builder: (context, workspaceProvider, canvasProvider, child) {
53-
// Listen for changes in the transformation to update position
5455
return ListenableBuilder(
5556
listenable: canvasProvider.transformationController,
5657
builder: (context, child) {
@@ -61,22 +62,19 @@ class WorkspaceDesktop extends StatelessWidget {
6162
final matrix =
6263
canvasProvider.transformationController.value;
6364

64-
// Use the matrix to find the object's top-center position on the screen
6565
final transformedTopCenter = matrix.transform3(
6666
vector_math.Vector3(objectBounds.topCenter.dx,
6767
objectBounds.topCenter.dy, 0));
6868

69-
// Calculate the screen position
7069
final screenPosition = Offset(
7170
transformedTopCenter.x, transformedTopCenter.y);
7271

73-
// Define an approximate size for the toolbox to help with centering.
7472
const double toolboxWidth = 240;
7573
const double toolboxHeight = 48;
7674

7775
return Positioned(
7876
left: screenPosition.dx - (toolboxWidth / 2),
79-
top: screenPosition.dy - toolboxHeight - 15, // 15px margin above object
77+
top: screenPosition.dy - toolboxHeight - 15,
8078
child: const NodeEditingToolbox(),
8179
);
8280
}

lib/features/workspace/pages/mobile/workspace_mobile.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ Widget workspaceDrawerMob(rh.DeviceType device) {
176176
onPressed: () {
177177
showDialog(
178178
context: context,
179-
builder: (context) => ExportDialog(su: suprovider),
179+
builder: (context) => ExportDialog(su: suprovider,wp: provider,),
180180
);
181181
},
182182
style: ElevatedButton.styleFrom(

0 commit comments

Comments
 (0)