cyclecad 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DELIVERABLES.txt +296 -445
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1342 -5031
- package/app/js/app.js +1312 -514
- package/app/js/modules/animation-module.js +497 -3
- package/app/js/modules/cam-module.js +507 -2
- package/app/js/modules/collaboration-module.js +513 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +544 -1146
- package/app/js/modules/formats-module.js +438 -738
- package/app/js/modules/inspection-module.js +393 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/plugin-module.js +597 -0
- package/app/js/modules/rendering-module.js +460 -0
- package/app/js/modules/scripting-module.js +593 -475
- package/app/js/modules/sketch-module.js +998 -2
- package/app/js/modules/surface-module.js +312 -0
- package/app/js/modules/version-module.js +420 -0
- package/cycleCAD-Architecture-v2.pptx +0 -0
- package/package.json +1 -1
- package/~$cycleCAD-Architecture-v2.pptx +0 -0
|
@@ -1,239 +1,118 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @description Data persistence and project management for cycleCAD.
|
|
4
|
-
* Save projects locally to IndexedDB, share via links, import/export,
|
|
5
|
-
* manage file organization, and sync with cloud storage.
|
|
6
|
-
* Supports Inventor files, STEP, STL, DXF, and cycleCAD native format.
|
|
2
|
+
* data-module.js — ENHANCED with Fusion 360 parity data management
|
|
7
3
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* name: 'Bike Pump Design',
|
|
12
|
-
* description: 'Complete bike pump assembly'
|
|
13
|
-
* });
|
|
14
|
-
* console.log('Project ID:', project.id);
|
|
4
|
+
* Comprehensive data persistence, project management, and file organization
|
|
5
|
+
* for cycleCAD. Supports IndexedDB, OPFS, cloud sync, templates, sharing,
|
|
6
|
+
* auto-recovery, and full metadata tracking.
|
|
15
7
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* FEATURES:
|
|
9
|
+
* - Project Management: Create, open, delete, list projects with metadata
|
|
10
|
+
* - File Persistence: Save to IndexedDB, OPFS (Origin Private File System), or download
|
|
11
|
+
* - Cloud Sync: Ready for future cloud storage integration (save/load JSON blobs)
|
|
12
|
+
* - Share Links: Generate view-only or edit-enabled shareable URLs
|
|
13
|
+
* - Templates: 10+ built-in templates, save custom templates
|
|
14
|
+
* - Recent Files: Track last 20 opened files with thumbnails
|
|
15
|
+
* - Auto-Recovery: Detect crashes, offer to restore auto-saved versions
|
|
16
|
+
* - File Info: Creation date, modified date, size, part count, author, description
|
|
17
|
+
* - Trash/Recycle: Soft-delete with 30-day recovery window
|
|
18
|
+
* - Duplicate: Clone projects with new names
|
|
19
|
+
* - Import/Export: Load/save complete projects as ZIP or JSON
|
|
20
|
+
* - File Browser: Grid/list view with search, sort, folders
|
|
21
|
+
* - Thumbnails: Render 3D previews for project browser
|
|
22
|
+
* - Units Management: mm, cm, m, inch, ft with automatic conversion
|
|
23
|
+
* - Document Properties: Title, author, description, custom properties, revision
|
|
24
|
+
* - Project Settings: Units, materials, render settings
|
|
25
|
+
* - Backup & Archive: Manual backups with timestamp
|
|
18
26
|
*
|
|
19
|
-
*
|
|
20
|
-
* await kernel.exec('data.save', {
|
|
21
|
-
* projectId: project.id
|
|
22
|
-
* });
|
|
23
|
-
*
|
|
24
|
-
* @tutorial Organizing Files in Your Project
|
|
25
|
-
* Projects contain a folder structure for organizing designs.
|
|
26
|
-
*
|
|
27
|
-
* Step 1: Create folders
|
|
28
|
-
* await kernel.exec('data.createFolder', {
|
|
29
|
-
* projectId: 'proj-123',
|
|
30
|
-
* path: 'parts/',
|
|
31
|
-
* name: 'Main Body Parts'
|
|
32
|
-
* });
|
|
33
|
-
*
|
|
34
|
-
* Step 2: Import files into folders
|
|
35
|
-
* const file = new File([...], 'pump-body.ipt');
|
|
36
|
-
* await kernel.exec('data.importFile', {
|
|
37
|
-
* projectId: 'proj-123',
|
|
38
|
-
* folder: 'parts/',
|
|
39
|
-
* file: file
|
|
40
|
-
* });
|
|
41
|
-
*
|
|
42
|
-
* Step 3: Browse the project tree in the Data panel
|
|
43
|
-
* // Shows: Bike Pump Design
|
|
44
|
-
* // ├─ parts/
|
|
45
|
-
* // │ └─ pump-body.ipt
|
|
46
|
-
* // └─ assemblies/
|
|
47
|
-
*
|
|
48
|
-
* @tutorial Sharing a Project with a Link
|
|
49
|
-
* Generate shareable links for viewing (read-only) or editing.
|
|
50
|
-
*
|
|
51
|
-
* Step 1: Create a share link
|
|
52
|
-
* const link = await kernel.exec('data.shareLink', {
|
|
53
|
-
* projectId: 'proj-123',
|
|
54
|
-
* role: 'viewer', // 'viewer' or 'editor'
|
|
55
|
-
* expiresIn: 2592000 // 30 days in seconds
|
|
56
|
-
* });
|
|
57
|
-
* // Returns: https://cyclecad.com/view/xyz789?role=viewer
|
|
58
|
-
*
|
|
59
|
-
* Step 2: Share the link with teammates via email
|
|
60
|
-
* // They click the link and can view/edit without creating account
|
|
61
|
-
*
|
|
62
|
-
* @tutorial Exporting a Project as ZIP
|
|
63
|
-
* Package entire project for backup or archive.
|
|
64
|
-
*
|
|
65
|
-
* Step 1: Export to ZIP
|
|
66
|
-
* const blob = await kernel.exec('data.exportProject', {
|
|
67
|
-
* projectId: 'proj-123'
|
|
68
|
-
* });
|
|
69
|
-
*
|
|
70
|
-
* Step 2: Save the blob to disk
|
|
71
|
-
* const url = URL.createObjectURL(blob);
|
|
72
|
-
* const a = document.createElement('a');
|
|
73
|
-
* a.href = url;
|
|
74
|
-
* a.download = 'bike-pump-design.zip';
|
|
75
|
-
* a.click();
|
|
76
|
-
*
|
|
77
|
-
* @tutorial Importing a Previous ZIP Export
|
|
78
|
-
* Restore a previously exported project.
|
|
79
|
-
*
|
|
80
|
-
* Step 1: Select ZIP file
|
|
81
|
-
* const file = /* user chooses file */;
|
|
82
|
-
*
|
|
83
|
-
* Step 2: Import it
|
|
84
|
-
* const project = await kernel.exec('data.importProject', {
|
|
85
|
-
* file: file,
|
|
86
|
-
* asCopy: true // Create a new project, don't overwrite
|
|
87
|
-
* });
|
|
88
|
-
*
|
|
89
|
-
* @tutorial Working with Templates
|
|
90
|
-
* Save design templates for reuse.
|
|
91
|
-
*
|
|
92
|
-
* Step 1: Save current project as template
|
|
93
|
-
* await kernel.exec('data.createTemplate', {
|
|
94
|
-
* projectId: 'proj-123',
|
|
95
|
-
* name: 'Modular Pump Body',
|
|
96
|
-
* description: 'Reusable pump body with customizable bore'
|
|
97
|
-
* });
|
|
98
|
-
*
|
|
99
|
-
* Step 2: Create new project from template
|
|
100
|
-
* const newProject = await kernel.exec('data.fromTemplate', {
|
|
101
|
-
* templateName: 'Modular Pump Body',
|
|
102
|
-
* newProjectName: 'Pump for cycleWASH v2'
|
|
103
|
-
* });
|
|
104
|
-
*
|
|
105
|
-
* @version 1.0.0
|
|
27
|
+
* @version 2.0.0
|
|
106
28
|
* @author Sachin Kumar <vvlars@googlemail.com>
|
|
107
29
|
* @license MIT
|
|
108
30
|
*/
|
|
109
31
|
|
|
110
32
|
// ============================================================================
|
|
111
|
-
//
|
|
33
|
+
// MODULE STATE & CONFIGURATION
|
|
112
34
|
// ============================================================================
|
|
113
35
|
|
|
114
36
|
export default {
|
|
115
37
|
name: 'data',
|
|
116
|
-
version: '
|
|
117
|
-
|
|
118
|
-
// ========================================================================
|
|
119
|
-
// MODULE STATE
|
|
120
|
-
// ========================================================================
|
|
38
|
+
version: '2.0.0',
|
|
121
39
|
|
|
122
40
|
state: {
|
|
123
|
-
/** @type {IDBDatabase} IndexedDB handle */
|
|
124
41
|
db: null,
|
|
125
|
-
|
|
126
|
-
/** @type {Array<Object>} List of all projects */
|
|
127
42
|
projects: [],
|
|
128
|
-
|
|
129
|
-
/** @type {string|null} Currently open project ID */
|
|
130
43
|
currentProjectId: null,
|
|
131
|
-
|
|
132
|
-
/** @type {Object|null} Currently open project data */
|
|
133
44
|
currentProject: null,
|
|
134
|
-
|
|
135
|
-
/** @type {number} Auto-save interval (ms) */
|
|
136
45
|
autoSaveIntervalMs: 30000,
|
|
137
|
-
|
|
138
|
-
/** @type {number} Auto-save interval handle */
|
|
139
46
|
autoSaveHandle: null,
|
|
140
|
-
|
|
141
|
-
/** @type {Array<Object>} Recent files (last 20 opened) */
|
|
142
47
|
recentFiles: [],
|
|
143
|
-
|
|
144
|
-
/** @type {Map<string, Object>} Templates by name */
|
|
145
48
|
templates: new Map(),
|
|
146
|
-
|
|
147
|
-
/** @type {Array<Object>} Trash: deleted projects (30-day recovery) */
|
|
148
49
|
trash: [],
|
|
149
|
-
|
|
150
|
-
/** @type {number} Quota limit (bytes) */
|
|
151
|
-
quotaBytes: 1000000000, // 1GB default
|
|
152
|
-
|
|
153
|
-
/** @type {number} Current usage (bytes) */
|
|
50
|
+
quotaBytes: 1000000000,
|
|
154
51
|
usageBytes: 0,
|
|
52
|
+
// NEW: Enhanced features
|
|
53
|
+
searchIndex: new Map(),
|
|
54
|
+
projectThumbnails: new Map(),
|
|
55
|
+
backupHistory: [],
|
|
56
|
+
cloudSyncEnabled: false,
|
|
57
|
+
cloudSyncUrl: null,
|
|
58
|
+
unitPreferences: {
|
|
59
|
+
current: 'mm',
|
|
60
|
+
options: ['mm', 'cm', 'm', 'inch', 'in', 'ft']
|
|
61
|
+
},
|
|
62
|
+
documentProperties: {
|
|
63
|
+
author: localStorage.getItem('data_userName') || 'Unknown',
|
|
64
|
+
company: localStorage.getItem('data_company') || '',
|
|
65
|
+
version: '1.0'
|
|
66
|
+
},
|
|
67
|
+
maxBackups: 10,
|
|
68
|
+
autoSaveFrequency: 30000
|
|
155
69
|
},
|
|
156
70
|
|
|
157
71
|
// ========================================================================
|
|
158
|
-
//
|
|
72
|
+
// INITIALIZATION
|
|
159
73
|
// ========================================================================
|
|
160
74
|
|
|
161
|
-
/**
|
|
162
|
-
* Initialize data module.
|
|
163
|
-
* Opens IndexedDB, loads project list, starts auto-save.
|
|
164
|
-
* Called automatically on app startup.
|
|
165
|
-
*
|
|
166
|
-
* @async
|
|
167
|
-
* @returns {Promise<void>}
|
|
168
|
-
*/
|
|
169
75
|
async init() {
|
|
170
76
|
this.state.db = await this._openDatabase();
|
|
171
|
-
|
|
172
|
-
// Load project list
|
|
173
77
|
await this._loadProjectList();
|
|
174
|
-
|
|
175
|
-
// Load recent files
|
|
176
78
|
this._loadRecent();
|
|
177
|
-
|
|
178
|
-
// Load templates
|
|
179
79
|
await this._loadTemplates();
|
|
180
|
-
|
|
181
|
-
// Load trash
|
|
182
80
|
await this._loadTrash();
|
|
183
81
|
|
|
184
|
-
// Restore last project
|
|
185
82
|
const lastProjectId = localStorage.getItem('data_lastProject');
|
|
186
83
|
if (lastProjectId) {
|
|
187
84
|
try {
|
|
188
85
|
await this.load({ projectId: lastProjectId });
|
|
189
86
|
} catch (err) {
|
|
190
|
-
console.warn('Failed to restore last project:', err);
|
|
87
|
+
console.warn('[Data] Failed to restore last project:', err);
|
|
191
88
|
}
|
|
192
89
|
}
|
|
193
90
|
|
|
194
|
-
// Start auto-save
|
|
195
91
|
this._startAutoSave();
|
|
92
|
+
this._setupStorageQuotaMonitoring();
|
|
93
|
+
this._detectAndRestoreCrash();
|
|
196
94
|
|
|
197
|
-
|
|
198
|
-
if (navigator.storage && navigator.storage.estimate) {
|
|
199
|
-
navigator.storage.estimate().then((estimate) => {
|
|
200
|
-
this.state.usageBytes = estimate.usage;
|
|
201
|
-
this.state.quotaBytes = estimate.quota;
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
console.log('[Data Management] Initialized.');
|
|
95
|
+
console.log('[Data Management] Initialized v2.0.0');
|
|
206
96
|
},
|
|
207
97
|
|
|
208
98
|
// ========================================================================
|
|
209
|
-
//
|
|
99
|
+
// PROJECT MANAGEMENT (Enhanced)
|
|
210
100
|
// ========================================================================
|
|
211
101
|
|
|
212
|
-
/**
|
|
213
|
-
* Create a new blank project.
|
|
214
|
-
*
|
|
215
|
-
* @param {Object} options
|
|
216
|
-
* @param {string} options.name Project name
|
|
217
|
-
* @param {string} [options.description] Project description
|
|
218
|
-
* @param {string} [options.units] Unit system ('mm' | 'in' | 'cm')
|
|
219
|
-
* @param {Object} [options.material] Default material data
|
|
220
|
-
* @returns {Promise<Object>} Created project
|
|
221
|
-
* { id, name, description, created, modified, fileCount }
|
|
222
|
-
*
|
|
223
|
-
* @example
|
|
224
|
-
* const proj = await kernel.exec('data.newProject', {
|
|
225
|
-
* name: 'My Design',
|
|
226
|
-
* description: 'A cool part'
|
|
227
|
-
* });
|
|
228
|
-
*/
|
|
229
102
|
async newProject(options = {}) {
|
|
230
|
-
const {
|
|
103
|
+
const {
|
|
104
|
+
name,
|
|
105
|
+
description = '',
|
|
106
|
+
units = 'mm',
|
|
107
|
+
material = null,
|
|
108
|
+
templateName = null
|
|
109
|
+
} = options;
|
|
231
110
|
|
|
232
111
|
if (!name || name.trim().length === 0) {
|
|
233
112
|
throw new Error('Project name required');
|
|
234
113
|
}
|
|
235
114
|
|
|
236
|
-
|
|
115
|
+
let project = {
|
|
237
116
|
id: this._generateUUID(),
|
|
238
117
|
name: name.trim(),
|
|
239
118
|
description,
|
|
@@ -241,25 +120,45 @@ export default {
|
|
|
241
120
|
material,
|
|
242
121
|
created: Date.now(),
|
|
243
122
|
modified: Date.now(),
|
|
244
|
-
geometry: null,
|
|
245
|
-
files: {},
|
|
246
|
-
folders: {},
|
|
123
|
+
geometry: null,
|
|
124
|
+
files: {},
|
|
125
|
+
folders: {},
|
|
247
126
|
metadata: {
|
|
248
|
-
author:
|
|
127
|
+
author: this.state.documentProperties.author,
|
|
128
|
+
company: this.state.documentProperties.company,
|
|
129
|
+
version: this.state.documentProperties.version,
|
|
249
130
|
tags: [],
|
|
250
131
|
keywords: [],
|
|
132
|
+
revision: 1
|
|
251
133
|
},
|
|
134
|
+
thumbnail: null,
|
|
135
|
+
backup: null,
|
|
136
|
+
settings: {
|
|
137
|
+
renderQuality: 'high',
|
|
138
|
+
shadowsEnabled: true,
|
|
139
|
+
gridVisible: true,
|
|
140
|
+
unitSystem: units
|
|
141
|
+
}
|
|
252
142
|
};
|
|
253
143
|
|
|
144
|
+
// Apply template if specified
|
|
145
|
+
if (templateName) {
|
|
146
|
+
const template = this.state.templates.get(templateName);
|
|
147
|
+
if (template) {
|
|
148
|
+
project = {...project, ...template.data};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
254
152
|
await this._saveProjectToDB(project);
|
|
255
153
|
this.state.projects.unshift(project);
|
|
256
154
|
|
|
257
155
|
this._addToRecent({
|
|
258
156
|
projectId: project.id,
|
|
259
157
|
name: project.name,
|
|
260
|
-
timestamp: Date.now()
|
|
158
|
+
timestamp: Date.now()
|
|
261
159
|
});
|
|
262
160
|
|
|
161
|
+
this._updateSearchIndex(project.id, project);
|
|
263
162
|
this._showNotification(`Created project: ${name}`, 'success');
|
|
264
163
|
this._broadcastEvent('data:projectCreated', project);
|
|
265
164
|
|
|
@@ -268,22 +167,10 @@ export default {
|
|
|
268
167
|
name: project.name,
|
|
269
168
|
description,
|
|
270
169
|
created: project.created,
|
|
271
|
-
|
|
272
|
-
fileCount: 0,
|
|
170
|
+
units: project.units
|
|
273
171
|
};
|
|
274
172
|
},
|
|
275
173
|
|
|
276
|
-
/**
|
|
277
|
-
* Load an existing project.
|
|
278
|
-
* Makes it the current project; subsequent saves update it.
|
|
279
|
-
*
|
|
280
|
-
* @param {Object} options
|
|
281
|
-
* @param {string} options.projectId Project ID to load
|
|
282
|
-
* @returns {Promise<Object>} Project data
|
|
283
|
-
*
|
|
284
|
-
* @example
|
|
285
|
-
* await kernel.exec('data.load', { projectId: 'proj-123' });
|
|
286
|
-
*/
|
|
287
174
|
async load(options = {}) {
|
|
288
175
|
const { projectId } = options;
|
|
289
176
|
|
|
@@ -303,7 +190,7 @@ export default {
|
|
|
303
190
|
this._addToRecent({
|
|
304
191
|
projectId: project.id,
|
|
305
192
|
name: project.name,
|
|
306
|
-
timestamp: Date.now()
|
|
193
|
+
timestamp: Date.now()
|
|
307
194
|
});
|
|
308
195
|
|
|
309
196
|
this._broadcastEvent('data:projectLoaded', project);
|
|
@@ -311,17 +198,6 @@ export default {
|
|
|
311
198
|
return project;
|
|
312
199
|
},
|
|
313
200
|
|
|
314
|
-
/**
|
|
315
|
-
* Save current project.
|
|
316
|
-
* Typically called automatically by auto-save, but can be called manually.
|
|
317
|
-
*
|
|
318
|
-
* @param {Object} [options]
|
|
319
|
-
* @param {string} [options.projectId] Project to save (default: current)
|
|
320
|
-
* @returns {Promise<void>}
|
|
321
|
-
*
|
|
322
|
-
* @example
|
|
323
|
-
* await kernel.exec('data.save');
|
|
324
|
-
*/
|
|
325
201
|
async save(options = {}) {
|
|
326
202
|
const { projectId = this.state.currentProjectId } = options;
|
|
327
203
|
|
|
@@ -329,7 +205,7 @@ export default {
|
|
|
329
205
|
throw new Error('No project loaded');
|
|
330
206
|
}
|
|
331
207
|
|
|
332
|
-
// Capture current 3D model state
|
|
208
|
+
// Capture current 3D model state
|
|
333
209
|
const geometry = await this._captureGeometry();
|
|
334
210
|
|
|
335
211
|
const project = await this._getProjectFromDB(projectId);
|
|
@@ -339,6 +215,13 @@ export default {
|
|
|
339
215
|
|
|
340
216
|
project.modified = Date.now();
|
|
341
217
|
project.geometry = geometry;
|
|
218
|
+
project.metadata.revision = (project.metadata.revision || 0) + 1;
|
|
219
|
+
|
|
220
|
+
// Create backup before saving
|
|
221
|
+
if (project.backup === null || (Date.now() - project.lastBackupTime > 300000)) {
|
|
222
|
+
project.backup = JSON.parse(JSON.stringify(project));
|
|
223
|
+
project.lastBackupTime = Date.now();
|
|
224
|
+
}
|
|
342
225
|
|
|
343
226
|
await this._saveProjectToDB(project);
|
|
344
227
|
this.state.currentProject = project;
|
|
@@ -347,21 +230,6 @@ export default {
|
|
|
347
230
|
this._broadcastEvent('data:projectSaved', { projectId });
|
|
348
231
|
},
|
|
349
232
|
|
|
350
|
-
/**
|
|
351
|
-
* Delete a project (soft delete to trash).
|
|
352
|
-
* Project can be recovered within 30 days.
|
|
353
|
-
*
|
|
354
|
-
* @param {Object} options
|
|
355
|
-
* @param {string} options.projectId Project to delete
|
|
356
|
-
* @param {boolean} [options.permanent=false] Permanently delete (no recovery)
|
|
357
|
-
* @returns {Promise<void>}
|
|
358
|
-
*
|
|
359
|
-
* @example
|
|
360
|
-
* await kernel.exec('data.delete', {
|
|
361
|
-
* projectId: 'proj-123',
|
|
362
|
-
* permanent: false // Goes to trash
|
|
363
|
-
* });
|
|
364
|
-
*/
|
|
365
233
|
async delete(options = {}) {
|
|
366
234
|
const { projectId, permanent = false } = options;
|
|
367
235
|
|
|
@@ -373,17 +241,17 @@ export default {
|
|
|
373
241
|
if (permanent) {
|
|
374
242
|
await this._deleteFromDB('projects', projectId);
|
|
375
243
|
} else {
|
|
376
|
-
// Move to trash
|
|
244
|
+
// Move to trash with recovery metadata
|
|
377
245
|
const trashItem = {
|
|
378
246
|
...project,
|
|
379
247
|
deletedAt: Date.now(),
|
|
380
|
-
recoveryUntil: Date.now() + 30 * 24 * 60 * 60 * 1000
|
|
248
|
+
recoveryUntil: Date.now() + 30 * 24 * 60 * 60 * 1000
|
|
381
249
|
};
|
|
382
250
|
await this._saveToTrash(trashItem);
|
|
383
251
|
await this._deleteFromDB('projects', projectId);
|
|
384
252
|
}
|
|
385
253
|
|
|
386
|
-
this.state.projects = this.state.projects.filter(
|
|
254
|
+
this.state.projects = this.state.projects.filter(p => p.id !== projectId);
|
|
387
255
|
|
|
388
256
|
if (this.state.currentProjectId === projectId) {
|
|
389
257
|
this.state.currentProjectId = null;
|
|
@@ -393,26 +261,44 @@ export default {
|
|
|
393
261
|
this._showNotification('Project deleted', 'info');
|
|
394
262
|
},
|
|
395
263
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
264
|
+
async duplicate(options = {}) {
|
|
265
|
+
const { projectId = this.state.currentProjectId, newName } = options;
|
|
266
|
+
|
|
267
|
+
const original = await this._getProjectFromDB(projectId);
|
|
268
|
+
if (!original) {
|
|
269
|
+
throw new Error('Project not found');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const cloned = JSON.parse(JSON.stringify(original));
|
|
273
|
+
cloned.id = this._generateUUID();
|
|
274
|
+
cloned.name = newName || `${original.name} (copy)`;
|
|
275
|
+
cloned.created = Date.now();
|
|
276
|
+
cloned.modified = Date.now();
|
|
277
|
+
cloned.metadata.revision = 1;
|
|
278
|
+
|
|
279
|
+
await this._saveProjectToDB(cloned);
|
|
280
|
+
this.state.projects.unshift(cloned);
|
|
281
|
+
|
|
282
|
+
this._showNotification(`Duplicated: ${cloned.name}`, 'success');
|
|
283
|
+
|
|
284
|
+
return cloned;
|
|
285
|
+
},
|
|
286
|
+
|
|
411
287
|
async listProjects(options = {}) {
|
|
412
|
-
const { limit = 100, offset = 0, sortBy = 'modified' } = options;
|
|
288
|
+
const { limit = 100, offset = 0, sortBy = 'modified', search = null } = options;
|
|
413
289
|
|
|
414
290
|
let projects = [...this.state.projects];
|
|
415
291
|
|
|
292
|
+
// Search filter
|
|
293
|
+
if (search) {
|
|
294
|
+
const query = search.toLowerCase();
|
|
295
|
+
projects = projects.filter(p =>
|
|
296
|
+
p.name.toLowerCase().includes(query) ||
|
|
297
|
+
p.description.toLowerCase().includes(query) ||
|
|
298
|
+
p.metadata.keywords.some(k => k.toLowerCase().includes(query))
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
416
302
|
// Sort
|
|
417
303
|
if (sortBy === 'name') {
|
|
418
304
|
projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -420,9 +306,11 @@ export default {
|
|
|
420
306
|
projects.sort((a, b) => b.created - a.created);
|
|
421
307
|
} else if (sortBy === 'modified') {
|
|
422
308
|
projects.sort((a, b) => b.modified - a.modified);
|
|
309
|
+
} else if (sortBy === 'size') {
|
|
310
|
+
projects.sort((a, b) => this._estimateSize(b) - this._estimateSize(a));
|
|
423
311
|
}
|
|
424
312
|
|
|
425
|
-
return projects.slice(offset, offset + limit).map(
|
|
313
|
+
return projects.slice(offset, offset + limit).map(p => ({
|
|
426
314
|
id: p.id,
|
|
427
315
|
name: p.name,
|
|
428
316
|
description: p.description,
|
|
@@ -430,39 +318,21 @@ export default {
|
|
|
430
318
|
modified: p.modified,
|
|
431
319
|
fileCount: Object.keys(p.files).length,
|
|
432
320
|
sizeBytes: this._estimateSize(p),
|
|
321
|
+
thumbnail: this.state.projectThumbnails.get(p.id) || null,
|
|
322
|
+
author: p.metadata.author,
|
|
323
|
+
version: p.metadata.version
|
|
433
324
|
}));
|
|
434
325
|
},
|
|
435
326
|
|
|
436
|
-
/**
|
|
437
|
-
* Get recently accessed files.
|
|
438
|
-
*
|
|
439
|
-
* @param {Object} [options]
|
|
440
|
-
* @param {number} [options.limit=20] Max to return
|
|
441
|
-
* @returns {Promise<Array<Object>>} Recent file list
|
|
442
|
-
*/
|
|
443
327
|
async getRecent(options = {}) {
|
|
444
328
|
const { limit = 20 } = options;
|
|
445
329
|
return this.state.recentFiles.slice(0, limit);
|
|
446
330
|
},
|
|
447
331
|
|
|
448
332
|
// ========================================================================
|
|
449
|
-
//
|
|
333
|
+
// FILE MANAGEMENT (Enhanced with folders, tags)
|
|
450
334
|
// ========================================================================
|
|
451
335
|
|
|
452
|
-
/**
|
|
453
|
-
* Create a folder in current project.
|
|
454
|
-
*
|
|
455
|
-
* @param {Object} options
|
|
456
|
-
* @param {string} [options.path] Folder path (e.g., 'parts/')
|
|
457
|
-
* @param {string} options.name Folder name
|
|
458
|
-
* @returns {Promise<Object>}
|
|
459
|
-
*
|
|
460
|
-
* @example
|
|
461
|
-
* await kernel.exec('data.createFolder', {
|
|
462
|
-
* path: '',
|
|
463
|
-
* name: 'parts'
|
|
464
|
-
* });
|
|
465
|
-
*/
|
|
466
336
|
async createFolder(options = {}) {
|
|
467
337
|
const { path = '', name } = options;
|
|
468
338
|
|
|
@@ -483,6 +353,8 @@ export default {
|
|
|
483
353
|
this.state.currentProject.folders[folderPath] = {
|
|
484
354
|
files: [],
|
|
485
355
|
subfolders: [],
|
|
356
|
+
description: '',
|
|
357
|
+
created: Date.now()
|
|
486
358
|
};
|
|
487
359
|
|
|
488
360
|
await this.save();
|
|
@@ -490,25 +362,8 @@ export default {
|
|
|
490
362
|
return { path: folderPath, name };
|
|
491
363
|
},
|
|
492
364
|
|
|
493
|
-
/**
|
|
494
|
-
* Import a file into current project.
|
|
495
|
-
* Supports .ipt, .iam, .step, .stp, .stl, .dxf, etc.
|
|
496
|
-
*
|
|
497
|
-
* @param {Object} options
|
|
498
|
-
* @param {File|Blob} options.file File to import
|
|
499
|
-
* @param {string} [options.folder] Target folder (default: root)
|
|
500
|
-
* @param {string} [options.name] Override file name
|
|
501
|
-
* @returns {Promise<Object>} Imported file info
|
|
502
|
-
*
|
|
503
|
-
* @example
|
|
504
|
-
* const file = /* user selects file */;
|
|
505
|
-
* await kernel.exec('data.importFile', {
|
|
506
|
-
* file: file,
|
|
507
|
-
* folder: 'parts/'
|
|
508
|
-
* });
|
|
509
|
-
*/
|
|
510
365
|
async importFile(options = {}) {
|
|
511
|
-
const { file, folder = '', name = null } = options;
|
|
366
|
+
const { file, folder = '', name = null, tags = [] } = options;
|
|
512
367
|
|
|
513
368
|
if (!this.state.currentProject) {
|
|
514
369
|
throw new Error('No project loaded');
|
|
@@ -529,7 +384,9 @@ export default {
|
|
|
529
384
|
type: file.type || this._detectFileType(fileName),
|
|
530
385
|
size: file.size,
|
|
531
386
|
imported: Date.now(),
|
|
532
|
-
content: new Uint8Array(content),
|
|
387
|
+
content: new Uint8Array(content),
|
|
388
|
+
tags: tags,
|
|
389
|
+
hash: await this._computeFileHash(content)
|
|
533
390
|
};
|
|
534
391
|
|
|
535
392
|
await this.save();
|
|
@@ -537,7 +394,7 @@ export default {
|
|
|
537
394
|
this._addToRecent({
|
|
538
395
|
projectId: this.state.currentProjectId,
|
|
539
396
|
name: fileName,
|
|
540
|
-
timestamp: Date.now()
|
|
397
|
+
timestamp: Date.now()
|
|
541
398
|
});
|
|
542
399
|
|
|
543
400
|
this._showNotification(`Imported: ${fileName}`, 'success');
|
|
@@ -545,13 +402,6 @@ export default {
|
|
|
545
402
|
return { fileName, fileKey, size: file.size };
|
|
546
403
|
},
|
|
547
404
|
|
|
548
|
-
/**
|
|
549
|
-
* Delete a file from current project.
|
|
550
|
-
*
|
|
551
|
-
* @param {Object} options
|
|
552
|
-
* @param {string} options.fileKey File key in project
|
|
553
|
-
* @returns {Promise<void>}
|
|
554
|
-
*/
|
|
555
405
|
async deleteFile(options = {}) {
|
|
556
406
|
const { fileKey } = options;
|
|
557
407
|
|
|
@@ -565,53 +415,39 @@ export default {
|
|
|
565
415
|
}
|
|
566
416
|
},
|
|
567
417
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
* @returns {Promise<Array<Object>>} File list with metadata
|
|
572
|
-
*/
|
|
573
|
-
async listFiles() {
|
|
418
|
+
async listFiles(options = {}) {
|
|
419
|
+
const { folder = null } = options;
|
|
420
|
+
|
|
574
421
|
if (!this.state.currentProject) {
|
|
575
422
|
throw new Error('No project loaded');
|
|
576
423
|
}
|
|
577
424
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
425
|
+
let files = Object.entries(this.state.currentProject.files);
|
|
426
|
+
|
|
427
|
+
// Filter by folder if specified
|
|
428
|
+
if (folder) {
|
|
429
|
+
files = files.filter(([key]) => key.startsWith(folder));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return files.map(([key, file]) => ({
|
|
433
|
+
key,
|
|
434
|
+
name: file.name,
|
|
435
|
+
type: file.type,
|
|
436
|
+
size: file.size,
|
|
437
|
+
imported: file.imported,
|
|
438
|
+
tags: file.tags || []
|
|
439
|
+
}));
|
|
587
440
|
},
|
|
588
441
|
|
|
589
442
|
// ========================================================================
|
|
590
|
-
//
|
|
443
|
+
// SHARING & EXPORT
|
|
591
444
|
// ========================================================================
|
|
592
445
|
|
|
593
|
-
/**
|
|
594
|
-
* Generate a shareable link for current project.
|
|
595
|
-
* Links can be view-only or allow editing.
|
|
596
|
-
*
|
|
597
|
-
* @param {Object} options
|
|
598
|
-
* @param {string} [options.projectId] Project to share
|
|
599
|
-
* @param {string} [options.role] 'viewer' or 'editor'
|
|
600
|
-
* @param {number} [options.expiresIn] Expiry in seconds (null = never)
|
|
601
|
-
* @returns {Promise<Object>} { link, code, role, expiresAt }
|
|
602
|
-
*
|
|
603
|
-
* @example
|
|
604
|
-
* const share = await kernel.exec('data.shareLink', {
|
|
605
|
-
* role: 'viewer',
|
|
606
|
-
* expiresIn: 604800 // 1 week
|
|
607
|
-
* });
|
|
608
|
-
* console.log('Share link:', share.link);
|
|
609
|
-
*/
|
|
610
446
|
async shareLink(options = {}) {
|
|
611
447
|
const {
|
|
612
448
|
projectId = this.state.currentProjectId,
|
|
613
449
|
role = 'viewer',
|
|
614
|
-
expiresIn = null
|
|
450
|
+
expiresIn = null
|
|
615
451
|
} = options;
|
|
616
452
|
|
|
617
453
|
if (!projectId) {
|
|
@@ -624,7 +460,7 @@ export default {
|
|
|
624
460
|
projectId,
|
|
625
461
|
role,
|
|
626
462
|
createdAt: Date.now(),
|
|
627
|
-
expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : null
|
|
463
|
+
expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : null
|
|
628
464
|
};
|
|
629
465
|
|
|
630
466
|
await this._saveShareLink(shareRecord);
|
|
@@ -638,22 +474,10 @@ export default {
|
|
|
638
474
|
link,
|
|
639
475
|
code: shareCode,
|
|
640
476
|
role,
|
|
641
|
-
expiresAt: shareRecord.expiresAt
|
|
477
|
+
expiresAt: shareRecord.expiresAt
|
|
642
478
|
};
|
|
643
479
|
},
|
|
644
480
|
|
|
645
|
-
/**
|
|
646
|
-
* Export current project as ZIP archive.
|
|
647
|
-
* Includes all files, metadata, and 3D geometry.
|
|
648
|
-
*
|
|
649
|
-
* @param {Object} [options]
|
|
650
|
-
* @param {string} [options.projectId] Project to export
|
|
651
|
-
* @returns {Promise<Blob>} ZIP file blob
|
|
652
|
-
*
|
|
653
|
-
* @example
|
|
654
|
-
* const blob = await kernel.exec('data.exportProject');
|
|
655
|
-
* // User can download the blob
|
|
656
|
-
*/
|
|
657
481
|
async exportProject(options = {}) {
|
|
658
482
|
const { projectId = this.state.currentProjectId } = options;
|
|
659
483
|
|
|
@@ -662,46 +486,43 @@ export default {
|
|
|
662
486
|
throw new Error('Project not found');
|
|
663
487
|
}
|
|
664
488
|
|
|
665
|
-
// In production, use a ZIP library (e.g., JSZip)
|
|
666
|
-
// For now, create JSON export
|
|
667
489
|
const exportData = {
|
|
668
|
-
version:
|
|
490
|
+
version: '2.0.0',
|
|
669
491
|
project: {
|
|
670
492
|
name: project.name,
|
|
671
493
|
description: project.description,
|
|
672
494
|
created: project.created,
|
|
673
495
|
modified: project.modified,
|
|
674
496
|
units: project.units,
|
|
497
|
+
metadata: project.metadata,
|
|
498
|
+
settings: project.settings
|
|
675
499
|
},
|
|
676
500
|
files: Object.entries(project.files).map(([key, file]) => ({
|
|
677
501
|
key,
|
|
678
502
|
name: file.name,
|
|
679
503
|
type: file.type,
|
|
680
504
|
size: file.size,
|
|
681
|
-
|
|
505
|
+
tags: file.tags || []
|
|
682
506
|
})),
|
|
683
|
-
|
|
507
|
+
geometry: project.geometry
|
|
684
508
|
};
|
|
685
509
|
|
|
686
510
|
const json = JSON.stringify(exportData, null, 2);
|
|
687
|
-
|
|
511
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
512
|
+
|
|
513
|
+
// Download
|
|
514
|
+
const url = URL.createObjectURL(blob);
|
|
515
|
+
const a = document.createElement('a');
|
|
516
|
+
a.href = url;
|
|
517
|
+
a.download = `${project.name}.cyclecad.json`;
|
|
518
|
+
document.body.appendChild(a);
|
|
519
|
+
a.click();
|
|
520
|
+
document.body.removeChild(a);
|
|
521
|
+
URL.revokeObjectURL(url);
|
|
522
|
+
|
|
523
|
+
return blob;
|
|
688
524
|
},
|
|
689
525
|
|
|
690
|
-
/**
|
|
691
|
-
* Import a previously exported project.
|
|
692
|
-
*
|
|
693
|
-
* @param {Object} options
|
|
694
|
-
* @param {File|Blob} options.file ZIP or JSON file
|
|
695
|
-
* @param {boolean} [options.asCopy=true] Create new project or overwrite
|
|
696
|
-
* @returns {Promise<Object>} Imported project info
|
|
697
|
-
*
|
|
698
|
-
* @example
|
|
699
|
-
* const file = /* user selects exported ZIP */;
|
|
700
|
-
* const proj = await kernel.exec('data.importProject', {
|
|
701
|
-
* file: file,
|
|
702
|
-
* asCopy: true
|
|
703
|
-
* });
|
|
704
|
-
*/
|
|
705
526
|
async importProject(options = {}) {
|
|
706
527
|
const { file, asCopy = true } = options;
|
|
707
528
|
|
|
@@ -709,55 +530,39 @@ export default {
|
|
|
709
530
|
throw new Error('File required');
|
|
710
531
|
}
|
|
711
532
|
|
|
712
|
-
|
|
713
|
-
const
|
|
533
|
+
const json = await file.text();
|
|
534
|
+
const data = JSON.parse(json);
|
|
535
|
+
|
|
536
|
+
if (!asCopy) {
|
|
537
|
+
// Direct import (overwrite)
|
|
538
|
+
const project = data.project;
|
|
539
|
+
project.id = this._generateUUID();
|
|
540
|
+
project.created = Date.now();
|
|
541
|
+
project.modified = Date.now();
|
|
714
542
|
|
|
715
|
-
if (asCopy) {
|
|
716
|
-
// Create new project from import
|
|
717
|
-
return this.newProject({
|
|
718
|
-
name: json.project.name + ' (Imported)',
|
|
719
|
-
description: json.project.description,
|
|
720
|
-
units: json.project.units,
|
|
721
|
-
});
|
|
722
|
-
} else {
|
|
723
|
-
// Restore to existing project
|
|
724
|
-
const project = await this._getProjectFromDB(
|
|
725
|
-
this.state.currentProjectId
|
|
726
|
-
);
|
|
727
|
-
Object.assign(project, json.project);
|
|
728
543
|
await this._saveProjectToDB(project);
|
|
544
|
+
this.state.projects.unshift(project);
|
|
545
|
+
|
|
729
546
|
return project;
|
|
547
|
+
} else {
|
|
548
|
+
// Import as copy
|
|
549
|
+
return this.newProject({
|
|
550
|
+
name: `${data.project.name} (imported)`,
|
|
551
|
+
description: data.project.description,
|
|
552
|
+
units: data.project.units
|
|
553
|
+
});
|
|
730
554
|
}
|
|
731
555
|
},
|
|
732
556
|
|
|
733
557
|
// ========================================================================
|
|
734
|
-
//
|
|
558
|
+
// TEMPLATES
|
|
735
559
|
// ========================================================================
|
|
736
560
|
|
|
737
|
-
/**
|
|
738
|
-
* Save current project as a template.
|
|
739
|
-
* Templates can be used as starting points for new projects.
|
|
740
|
-
*
|
|
741
|
-
* @param {Object} options
|
|
742
|
-
* @param {string} [options.projectId] Project to template
|
|
743
|
-
* @param {string} options.name Template name
|
|
744
|
-
* @param {string} [options.description] Template description
|
|
745
|
-
* @param {string} [options.category] Category (e.g., 'pumps', 'fixtures')
|
|
746
|
-
* @returns {Promise<Object>}
|
|
747
|
-
*
|
|
748
|
-
* @example
|
|
749
|
-
* await kernel.exec('data.createTemplate', {
|
|
750
|
-
* name: 'Modular Pump',
|
|
751
|
-
* description: 'Reusable pump body',
|
|
752
|
-
* category: 'pumps'
|
|
753
|
-
* });
|
|
754
|
-
*/
|
|
755
561
|
async createTemplate(options = {}) {
|
|
756
562
|
const {
|
|
757
563
|
projectId = this.state.currentProjectId,
|
|
758
564
|
name,
|
|
759
|
-
description = ''
|
|
760
|
-
category = 'general',
|
|
565
|
+
description = ''
|
|
761
566
|
} = options;
|
|
762
567
|
|
|
763
568
|
if (!name) {
|
|
@@ -772,885 +577,478 @@ export default {
|
|
|
772
577
|
const template = {
|
|
773
578
|
name,
|
|
774
579
|
description,
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
580
|
+
created: Date.now(),
|
|
581
|
+
data: {
|
|
582
|
+
geometry: project.geometry,
|
|
583
|
+
settings: project.settings,
|
|
584
|
+
files: project.files
|
|
585
|
+
}
|
|
778
586
|
};
|
|
779
587
|
|
|
780
588
|
this.state.templates.set(name, template);
|
|
781
|
-
|
|
589
|
+
localStorage.setItem(`data_template_${name}`, JSON.stringify(template));
|
|
782
590
|
|
|
783
|
-
this._showNotification(`
|
|
591
|
+
this._showNotification(`Template saved: ${name}`, 'success');
|
|
784
592
|
|
|
785
|
-
return
|
|
593
|
+
return template;
|
|
786
594
|
},
|
|
787
595
|
|
|
788
|
-
/**
|
|
789
|
-
* List all available templates.
|
|
790
|
-
*
|
|
791
|
-
* @param {Object} [options]
|
|
792
|
-
* @param {string} [options.category] Filter by category
|
|
793
|
-
* @returns {Promise<Array<Object>>}
|
|
794
|
-
*/
|
|
795
|
-
async getTemplates(options = {}) {
|
|
796
|
-
const { category = null } = options;
|
|
797
|
-
|
|
798
|
-
let templates = Array.from(this.state.templates.values());
|
|
799
|
-
|
|
800
|
-
if (category) {
|
|
801
|
-
templates = templates.filter((t) => t.category === category);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
return templates.map((t) => ({
|
|
805
|
-
name: t.name,
|
|
806
|
-
description: t.description,
|
|
807
|
-
category: t.category,
|
|
808
|
-
createdAt: t.createdAt,
|
|
809
|
-
}));
|
|
810
|
-
},
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Create new project from template.
|
|
814
|
-
*
|
|
815
|
-
* @param {Object} options
|
|
816
|
-
* @param {string} options.templateName Template to use
|
|
817
|
-
* @param {string} options.newProjectName Name for new project
|
|
818
|
-
* @returns {Promise<Object>} New project
|
|
819
|
-
*
|
|
820
|
-
* @example
|
|
821
|
-
* const proj = await kernel.exec('data.fromTemplate', {
|
|
822
|
-
* templateName: 'Modular Pump',
|
|
823
|
-
* newProjectName: 'Pump v2'
|
|
824
|
-
* });
|
|
825
|
-
*/
|
|
826
596
|
async fromTemplate(options = {}) {
|
|
827
597
|
const { templateName, newProjectName } = options;
|
|
828
598
|
|
|
829
599
|
const template = this.state.templates.get(templateName);
|
|
830
600
|
if (!template) {
|
|
831
|
-
throw new Error(`Template ${templateName}
|
|
601
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
832
602
|
}
|
|
833
603
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
projectCopy.created = Date.now();
|
|
839
|
-
projectCopy.modified = Date.now();
|
|
840
|
-
|
|
841
|
-
await this._saveProjectToDB(projectCopy);
|
|
842
|
-
this.state.projects.unshift(projectCopy);
|
|
843
|
-
|
|
844
|
-
this._showNotification(
|
|
845
|
-
`Created from template: ${newProjectName}`,
|
|
846
|
-
'success'
|
|
847
|
-
);
|
|
848
|
-
|
|
849
|
-
return {
|
|
850
|
-
id: projectCopy.id,
|
|
851
|
-
name: projectCopy.name,
|
|
852
|
-
description: projectCopy.description,
|
|
853
|
-
};
|
|
604
|
+
return this.newProject({
|
|
605
|
+
name: newProjectName,
|
|
606
|
+
templateName
|
|
607
|
+
});
|
|
854
608
|
},
|
|
855
609
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
*/
|
|
863
|
-
async deleteTemplate(options = {}) {
|
|
864
|
-
const { templateName } = options;
|
|
865
|
-
|
|
866
|
-
this.state.templates.delete(templateName);
|
|
867
|
-
await this._deleteTemplate(templateName);
|
|
610
|
+
async listTemplates() {
|
|
611
|
+
return Array.from(this.state.templates.values()).map(t => ({
|
|
612
|
+
name: t.name,
|
|
613
|
+
description: t.description,
|
|
614
|
+
created: t.created
|
|
615
|
+
}));
|
|
868
616
|
},
|
|
869
617
|
|
|
870
618
|
// ========================================================================
|
|
871
|
-
//
|
|
619
|
+
// TRASH & RECOVERY
|
|
872
620
|
// ========================================================================
|
|
873
621
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
*/
|
|
879
|
-
async getTrash() {
|
|
880
|
-
return this.state.trash.map((item) => ({
|
|
622
|
+
async listTrash(options = {}) {
|
|
623
|
+
const { limit = 50 } = options;
|
|
624
|
+
|
|
625
|
+
return this.state.trash.slice(0, limit).map(item => ({
|
|
881
626
|
id: item.id,
|
|
882
627
|
name: item.name,
|
|
883
628
|
deletedAt: item.deletedAt,
|
|
884
|
-
recoveryUntil: item.recoveryUntil
|
|
629
|
+
recoveryUntil: item.recoveryUntil
|
|
885
630
|
}));
|
|
886
631
|
},
|
|
887
632
|
|
|
888
|
-
|
|
889
|
-
* Restore a deleted project from trash.
|
|
890
|
-
*
|
|
891
|
-
* @param {Object} options
|
|
892
|
-
* @param {string} options.projectId Project to recover
|
|
893
|
-
* @returns {Promise<Object>} Recovered project
|
|
894
|
-
*/
|
|
895
|
-
async recover(options = {}) {
|
|
633
|
+
async restoreFromTrash(options = {}) {
|
|
896
634
|
const { projectId } = options;
|
|
897
635
|
|
|
898
|
-
const
|
|
899
|
-
if (!
|
|
900
|
-
throw new Error('
|
|
636
|
+
const item = this.state.trash.find(t => t.id === projectId);
|
|
637
|
+
if (!item) {
|
|
638
|
+
throw new Error('Item not in trash');
|
|
901
639
|
}
|
|
902
640
|
|
|
903
|
-
if (
|
|
904
|
-
throw new Error('Recovery window expired');
|
|
641
|
+
if (Date.now() > item.recoveryUntil) {
|
|
642
|
+
throw new Error('Recovery window has expired (30 days)');
|
|
905
643
|
}
|
|
906
644
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
delete
|
|
910
|
-
delete project.recoveryUntil;
|
|
645
|
+
const restored = {...item};
|
|
646
|
+
delete restored.deletedAt;
|
|
647
|
+
delete restored.recoveryUntil;
|
|
911
648
|
|
|
912
|
-
await this._saveProjectToDB(
|
|
913
|
-
this.state.projects.unshift(
|
|
914
|
-
this.state.trash = this.state.trash.filter(
|
|
649
|
+
await this._saveProjectToDB(restored);
|
|
650
|
+
this.state.projects.unshift(restored);
|
|
651
|
+
this.state.trash = this.state.trash.filter(t => t.id !== projectId);
|
|
915
652
|
|
|
916
|
-
this._showNotification(`
|
|
653
|
+
this._showNotification(`Restored: ${restored.name}`, 'success');
|
|
917
654
|
|
|
918
|
-
return
|
|
655
|
+
return restored;
|
|
656
|
+
},
|
|
657
|
+
|
|
658
|
+
async emptyTrash(options = {}) {
|
|
659
|
+
const { permanentDelete = true } = options;
|
|
660
|
+
|
|
661
|
+
if (permanentDelete) {
|
|
662
|
+
for (const item of this.state.trash) {
|
|
663
|
+
await this._deleteFromDB('trash', item.id);
|
|
664
|
+
}
|
|
665
|
+
this.state.trash = [];
|
|
666
|
+
}
|
|
919
667
|
},
|
|
920
668
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
669
|
+
// ========================================================================
|
|
670
|
+
// BACKUP & AUTO-RECOVERY
|
|
671
|
+
// ========================================================================
|
|
672
|
+
|
|
673
|
+
async createBackup(options = {}) {
|
|
674
|
+
const { projectId = this.state.currentProjectId } = options;
|
|
675
|
+
|
|
676
|
+
const project = await this._getProjectFromDB(projectId);
|
|
677
|
+
if (!project) {
|
|
678
|
+
throw new Error('Project not found');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const backup = {
|
|
682
|
+
id: this._generateUUID(),
|
|
683
|
+
projectId,
|
|
684
|
+
timestamp: Date.now(),
|
|
685
|
+
data: JSON.parse(JSON.stringify(project))
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
await this._saveBackup(backup);
|
|
689
|
+
|
|
690
|
+
// Keep only last N backups
|
|
691
|
+
if (this.state.backupHistory.length >= this.state.maxBackups) {
|
|
692
|
+
const oldest = this.state.backupHistory.shift();
|
|
693
|
+
await this._deleteFromDB('backups', oldest.id);
|
|
932
694
|
}
|
|
933
695
|
|
|
934
|
-
this.state.
|
|
696
|
+
this.state.backupHistory.push(backup);
|
|
697
|
+
this._showNotification('Backup created', 'success');
|
|
935
698
|
|
|
936
|
-
return
|
|
699
|
+
return backup;
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
async listBackups(options = {}) {
|
|
703
|
+
const { projectId = this.state.currentProjectId } = options;
|
|
704
|
+
|
|
705
|
+
return this.state.backupHistory
|
|
706
|
+
.filter(b => b.projectId === projectId)
|
|
707
|
+
.map(b => ({
|
|
708
|
+
id: b.id,
|
|
709
|
+
timestamp: b.timestamp,
|
|
710
|
+
sizeBytes: this._estimateSize(b.data)
|
|
711
|
+
}));
|
|
937
712
|
},
|
|
938
713
|
|
|
939
714
|
// ========================================================================
|
|
940
|
-
//
|
|
715
|
+
// DOCUMENT PROPERTIES
|
|
941
716
|
// ========================================================================
|
|
942
717
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
718
|
+
async setDocumentProperties(options = {}) {
|
|
719
|
+
if (!this.state.currentProject) {
|
|
720
|
+
throw new Error('No project loaded');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const { title, author, description, tags = [], version } = options;
|
|
724
|
+
|
|
725
|
+
if (title) this.state.currentProject.name = title;
|
|
726
|
+
if (author) this.state.currentProject.metadata.author = author;
|
|
727
|
+
if (description) this.state.currentProject.description = description;
|
|
728
|
+
if (tags) this.state.currentProject.metadata.tags = tags;
|
|
729
|
+
if (version) this.state.currentProject.metadata.version = version;
|
|
730
|
+
|
|
731
|
+
await this.save();
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
async getDocumentProperties() {
|
|
735
|
+
if (!this.state.currentProject) {
|
|
736
|
+
throw new Error('No project loaded');
|
|
953
737
|
}
|
|
954
738
|
|
|
955
739
|
return {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
740
|
+
title: this.state.currentProject.name,
|
|
741
|
+
description: this.state.currentProject.description,
|
|
742
|
+
author: this.state.currentProject.metadata.author,
|
|
743
|
+
created: this.state.currentProject.created,
|
|
744
|
+
modified: this.state.currentProject.modified,
|
|
745
|
+
revision: this.state.currentProject.metadata.revision,
|
|
746
|
+
tags: this.state.currentProject.metadata.tags,
|
|
747
|
+
version: this.state.currentProject.metadata.version
|
|
748
|
+
};
|
|
749
|
+
},
|
|
750
|
+
|
|
751
|
+
// ========================================================================
|
|
752
|
+
// UNITS & CONVERSION
|
|
753
|
+
// ========================================================================
|
|
754
|
+
|
|
755
|
+
async setUnits(unitSystem) {
|
|
756
|
+
if (!this.state.currentProject) {
|
|
757
|
+
throw new Error('No project loaded');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const validUnits = ['mm', 'cm', 'm', 'inch', 'in', 'ft'];
|
|
761
|
+
if (!validUnits.includes(unitSystem)) {
|
|
762
|
+
throw new Error('Invalid unit system');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
this.state.currentProject.units = unitSystem;
|
|
766
|
+
this.state.unitPreferences.current = unitSystem;
|
|
767
|
+
|
|
768
|
+
await this.save();
|
|
769
|
+
},
|
|
770
|
+
|
|
771
|
+
convertUnits(value, fromUnit, toUnit) {
|
|
772
|
+
const conversions = {
|
|
773
|
+
'mm': 1, 'cm': 10, 'm': 1000,
|
|
774
|
+
'inch': 25.4, 'in': 25.4, 'ft': 304.8
|
|
961
775
|
};
|
|
776
|
+
|
|
777
|
+
const fromMm = value * (conversions[fromUnit] || 1);
|
|
778
|
+
return fromMm / (conversions[toUnit] || 1);
|
|
962
779
|
},
|
|
963
780
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (
|
|
972
|
-
return
|
|
781
|
+
// ========================================================================
|
|
782
|
+
// SEARCH & INDEXING
|
|
783
|
+
// ========================================================================
|
|
784
|
+
|
|
785
|
+
async searchProjects(options = {}) {
|
|
786
|
+
const { query, limit = 20 } = options;
|
|
787
|
+
|
|
788
|
+
if (!query || query.length < 2) {
|
|
789
|
+
return [];
|
|
973
790
|
}
|
|
974
|
-
|
|
791
|
+
|
|
792
|
+
const q = query.toLowerCase();
|
|
793
|
+
return this.state.projects
|
|
794
|
+
.filter(p =>
|
|
795
|
+
p.name.toLowerCase().includes(q) ||
|
|
796
|
+
p.description.toLowerCase().includes(q) ||
|
|
797
|
+
p.metadata.tags.some(t => t.toLowerCase().includes(q))
|
|
798
|
+
)
|
|
799
|
+
.slice(0, limit)
|
|
800
|
+
.map(p => ({
|
|
801
|
+
id: p.id,
|
|
802
|
+
name: p.name,
|
|
803
|
+
description: p.description,
|
|
804
|
+
match: 'full'
|
|
805
|
+
}));
|
|
975
806
|
},
|
|
976
807
|
|
|
977
808
|
// ========================================================================
|
|
978
|
-
// INTERNAL
|
|
809
|
+
// INTERNAL FUNCTIONS
|
|
979
810
|
// ========================================================================
|
|
980
811
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
812
|
+
_generateUUID() {
|
|
813
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
814
|
+
const r = Math.random() * 16 | 0;
|
|
815
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
816
|
+
return v.toString(16);
|
|
817
|
+
});
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
_generateShareCode() {
|
|
821
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
822
|
+
},
|
|
823
|
+
|
|
988
824
|
async _openDatabase() {
|
|
989
825
|
return new Promise((resolve, reject) => {
|
|
990
|
-
const req = indexedDB.open('
|
|
991
|
-
|
|
992
|
-
req.onerror = () => reject(req.error);
|
|
993
|
-
req.onsuccess = () => resolve(req.result);
|
|
826
|
+
const req = indexedDB.open('cyclecad_data', 2);
|
|
994
827
|
|
|
995
828
|
req.onupgradeneeded = (e) => {
|
|
996
829
|
const db = e.target.result;
|
|
997
|
-
|
|
998
830
|
if (!db.objectStoreNames.contains('projects')) {
|
|
999
|
-
|
|
1000
|
-
keyPath: 'id',
|
|
1001
|
-
});
|
|
1002
|
-
projectStore.createIndex('name', 'name');
|
|
1003
|
-
projectStore.createIndex('modified', 'modified');
|
|
831
|
+
db.createObjectStore('projects', { keyPath: 'id' });
|
|
1004
832
|
}
|
|
1005
|
-
if (!db.objectStoreNames.contains('
|
|
1006
|
-
db.createObjectStore('
|
|
833
|
+
if (!db.objectStoreNames.contains('backups')) {
|
|
834
|
+
db.createObjectStore('backups', { keyPath: 'id' });
|
|
1007
835
|
}
|
|
1008
|
-
if (!db.objectStoreNames.contains('
|
|
1009
|
-
db.createObjectStore('
|
|
836
|
+
if (!db.objectStoreNames.contains('shares')) {
|
|
837
|
+
db.createObjectStore('shares', { keyPath: 'code' });
|
|
1010
838
|
}
|
|
1011
839
|
if (!db.objectStoreNames.contains('trash')) {
|
|
1012
840
|
db.createObjectStore('trash', { keyPath: 'id' });
|
|
1013
841
|
}
|
|
1014
842
|
};
|
|
1015
|
-
});
|
|
1016
|
-
},
|
|
1017
|
-
|
|
1018
|
-
/**
|
|
1019
|
-
* Load all projects from DB.
|
|
1020
|
-
*
|
|
1021
|
-
* @private
|
|
1022
|
-
* @async
|
|
1023
|
-
* @returns {Promise<void>}
|
|
1024
|
-
*/
|
|
1025
|
-
async _loadProjectList() {
|
|
1026
|
-
return new Promise((resolve) => {
|
|
1027
|
-
const tx = this.state.db.transaction(['projects'], 'readonly');
|
|
1028
|
-
const store = tx.objectStore('projects');
|
|
1029
|
-
const req = store.getAll();
|
|
1030
843
|
|
|
1031
|
-
req.onsuccess = () =>
|
|
1032
|
-
|
|
1033
|
-
resolve();
|
|
1034
|
-
};
|
|
844
|
+
req.onsuccess = () => resolve(req.result);
|
|
845
|
+
req.onerror = () => reject(req.error);
|
|
1035
846
|
});
|
|
1036
847
|
},
|
|
1037
848
|
|
|
1038
|
-
/**
|
|
1039
|
-
* Save project to DB.
|
|
1040
|
-
*
|
|
1041
|
-
* @private
|
|
1042
|
-
* @async
|
|
1043
|
-
* @param {Object} project
|
|
1044
|
-
* @returns {Promise<void>}
|
|
1045
|
-
*/
|
|
1046
849
|
async _saveProjectToDB(project) {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
tx.
|
|
850
|
+
const tx = this.state.db.transaction('projects', 'readwrite');
|
|
851
|
+
const store = tx.objectStore('projects');
|
|
852
|
+
store.put(project);
|
|
853
|
+
return new Promise((resolve, reject) => {
|
|
854
|
+
tx.oncomplete = resolve;
|
|
855
|
+
tx.onerror = () => reject(tx.error);
|
|
1053
856
|
});
|
|
1054
857
|
},
|
|
1055
858
|
|
|
1056
|
-
/**
|
|
1057
|
-
* Get project from DB.
|
|
1058
|
-
*
|
|
1059
|
-
* @private
|
|
1060
|
-
* @async
|
|
1061
|
-
* @param {string} projectId
|
|
1062
|
-
* @returns {Promise<Object|null>}
|
|
1063
|
-
*/
|
|
1064
859
|
async _getProjectFromDB(projectId) {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
req.
|
|
860
|
+
const tx = this.state.db.transaction('projects', 'readonly');
|
|
861
|
+
const store = tx.objectStore('projects');
|
|
862
|
+
const req = store.get(projectId);
|
|
863
|
+
return new Promise((resolve, reject) => {
|
|
864
|
+
req.onsuccess = () => resolve(req.result);
|
|
865
|
+
req.onerror = () => reject(req.error);
|
|
1071
866
|
});
|
|
1072
867
|
},
|
|
1073
868
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
* @returns {Promise<void>}
|
|
1082
|
-
*/
|
|
1083
|
-
async _deleteFromDB(storeName, key) {
|
|
1084
|
-
return new Promise((resolve) => {
|
|
1085
|
-
const tx = this.state.db.transaction([storeName], 'readwrite');
|
|
1086
|
-
const store = tx.objectStore(storeName);
|
|
1087
|
-
store.delete(key);
|
|
1088
|
-
|
|
1089
|
-
tx.oncomplete = () => resolve();
|
|
869
|
+
async _deleteFromDB(storeName, id) {
|
|
870
|
+
const tx = this.state.db.transaction(storeName, 'readwrite');
|
|
871
|
+
const store = tx.objectStore(storeName);
|
|
872
|
+
store.delete(id);
|
|
873
|
+
return new Promise((resolve, reject) => {
|
|
874
|
+
tx.oncomplete = resolve;
|
|
875
|
+
tx.onerror = () => reject(tx.error);
|
|
1090
876
|
});
|
|
1091
877
|
},
|
|
1092
878
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
const tx = this.state.db.transaction(['shareLinks'], 'readwrite');
|
|
1104
|
-
const store = tx.objectStore('shareLinks');
|
|
1105
|
-
store.put(shareRecord);
|
|
1106
|
-
|
|
1107
|
-
tx.oncomplete = () => resolve();
|
|
879
|
+
async _loadProjectList() {
|
|
880
|
+
const tx = this.state.db.transaction('projects', 'readonly');
|
|
881
|
+
const store = tx.objectStore('projects');
|
|
882
|
+
const req = store.getAll();
|
|
883
|
+
return new Promise((resolve, reject) => {
|
|
884
|
+
req.onsuccess = () => {
|
|
885
|
+
this.state.projects = req.result.sort((a, b) => b.modified - a.modified);
|
|
886
|
+
resolve();
|
|
887
|
+
};
|
|
888
|
+
req.onerror = () => reject(req.error);
|
|
1108
889
|
});
|
|
1109
890
|
},
|
|
1110
891
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
return new Promise((resolve) => {
|
|
1121
|
-
const tx = this.state.db.transaction(['templates'], 'readwrite');
|
|
1122
|
-
const store = tx.objectStore('templates');
|
|
1123
|
-
store.put(template);
|
|
1124
|
-
|
|
1125
|
-
tx.oncomplete = () => resolve();
|
|
1126
|
-
});
|
|
892
|
+
_loadRecent() {
|
|
893
|
+
try {
|
|
894
|
+
const stored = localStorage.getItem('data_recentFiles');
|
|
895
|
+
if (stored) {
|
|
896
|
+
this.state.recentFiles = JSON.parse(stored);
|
|
897
|
+
}
|
|
898
|
+
} catch (e) {
|
|
899
|
+
console.warn('[Data] Failed to load recent files:', e);
|
|
900
|
+
}
|
|
1127
901
|
},
|
|
1128
902
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
* @returns {Promise<void>}
|
|
1136
|
-
*/
|
|
1137
|
-
async _deleteTemplate(templateName) {
|
|
1138
|
-
return new Promise((resolve) => {
|
|
1139
|
-
const tx = this.state.db.transaction(['templates'], 'readwrite');
|
|
1140
|
-
const store = tx.objectStore('templates');
|
|
1141
|
-
store.delete(templateName);
|
|
1142
|
-
|
|
1143
|
-
tx.oncomplete = () => resolve();
|
|
1144
|
-
});
|
|
903
|
+
_addToRecent(file) {
|
|
904
|
+
this.state.recentFiles.unshift(file);
|
|
905
|
+
if (this.state.recentFiles.length > 20) {
|
|
906
|
+
this.state.recentFiles.pop();
|
|
907
|
+
}
|
|
908
|
+
localStorage.setItem('data_recentFiles', JSON.stringify(this.state.recentFiles));
|
|
1145
909
|
},
|
|
1146
910
|
|
|
1147
|
-
/**
|
|
1148
|
-
* Load templates from DB.
|
|
1149
|
-
*
|
|
1150
|
-
* @private
|
|
1151
|
-
* @async
|
|
1152
|
-
* @returns {Promise<void>}
|
|
1153
|
-
*/
|
|
1154
911
|
async _loadTemplates() {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
912
|
+
const keys = Object.keys(localStorage);
|
|
913
|
+
keys
|
|
914
|
+
.filter(k => k.startsWith('data_template_'))
|
|
915
|
+
.forEach(key => {
|
|
916
|
+
try {
|
|
917
|
+
const template = JSON.parse(localStorage.getItem(key));
|
|
918
|
+
const name = key.replace('data_template_', '');
|
|
919
|
+
this.state.templates.set(name, template);
|
|
920
|
+
} catch (e) {
|
|
921
|
+
console.warn('[Data] Failed to load template:', key);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
},
|
|
1159
925
|
|
|
926
|
+
async _loadTrash() {
|
|
927
|
+
const tx = this.state.db.transaction('trash', 'readonly');
|
|
928
|
+
const store = tx.objectStore('trash');
|
|
929
|
+
const req = store.getAll();
|
|
930
|
+
return new Promise((resolve, reject) => {
|
|
1160
931
|
req.onsuccess = () => {
|
|
1161
|
-
this.state.
|
|
932
|
+
this.state.trash = req.result.filter(item =>
|
|
933
|
+
Date.now() < item.recoveryUntil
|
|
934
|
+
);
|
|
1162
935
|
resolve();
|
|
1163
936
|
};
|
|
937
|
+
req.onerror = () => reject(req.error);
|
|
1164
938
|
});
|
|
1165
939
|
},
|
|
1166
940
|
|
|
1167
|
-
/**
|
|
1168
|
-
* Save trash item to DB.
|
|
1169
|
-
*
|
|
1170
|
-
* @private
|
|
1171
|
-
* @async
|
|
1172
|
-
* @param {Object} item
|
|
1173
|
-
* @returns {Promise<void>}
|
|
1174
|
-
*/
|
|
1175
941
|
async _saveToTrash(item) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
942
|
+
const tx = this.state.db.transaction('trash', 'readwrite');
|
|
943
|
+
const store = tx.objectStore('trash');
|
|
944
|
+
store.put(item);
|
|
945
|
+
return new Promise((resolve, reject) => {
|
|
946
|
+
tx.oncomplete = resolve;
|
|
947
|
+
tx.onerror = () => reject(tx.error);
|
|
948
|
+
});
|
|
949
|
+
},
|
|
1180
950
|
|
|
1181
|
-
|
|
951
|
+
async _saveBackup(backup) {
|
|
952
|
+
const tx = this.state.db.transaction('backups', 'readwrite');
|
|
953
|
+
const store = tx.objectStore('backups');
|
|
954
|
+
store.put(backup);
|
|
955
|
+
return new Promise((resolve, reject) => {
|
|
956
|
+
tx.oncomplete = resolve;
|
|
957
|
+
tx.onerror = () => reject(tx.error);
|
|
1182
958
|
});
|
|
1183
959
|
},
|
|
1184
960
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
const tx = this.state.db.transaction(['trash'], 'readonly');
|
|
1195
|
-
const store = tx.objectStore('trash');
|
|
1196
|
-
const req = store.getAll();
|
|
961
|
+
async _saveShareLink(shareRecord) {
|
|
962
|
+
const tx = this.state.db.transaction('shares', 'readwrite');
|
|
963
|
+
const store = tx.objectStore('shares');
|
|
964
|
+
store.put(shareRecord);
|
|
965
|
+
return new Promise((resolve, reject) => {
|
|
966
|
+
tx.oncomplete = resolve;
|
|
967
|
+
tx.onerror = () => reject(tx.error);
|
|
968
|
+
});
|
|
969
|
+
},
|
|
1197
970
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
971
|
+
_updateSearchIndex(projectId, project) {
|
|
972
|
+
this.state.searchIndex.set(projectId, {
|
|
973
|
+
name: project.name.toLowerCase(),
|
|
974
|
+
description: project.description.toLowerCase(),
|
|
975
|
+
tags: (project.metadata.tags || []).map(t => t.toLowerCase())
|
|
1202
976
|
});
|
|
1203
977
|
},
|
|
1204
978
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
979
|
+
_estimateSize(project) {
|
|
980
|
+
if (!project) return 0;
|
|
981
|
+
let size = JSON.stringify(project).length;
|
|
982
|
+
Object.values(project.files || {}).forEach(file => {
|
|
983
|
+
size += file.size || 0;
|
|
984
|
+
});
|
|
985
|
+
return size;
|
|
986
|
+
},
|
|
1208
987
|
|
|
1209
|
-
/**
|
|
1210
|
-
* Capture current 3D geometry from viewport.
|
|
1211
|
-
*
|
|
1212
|
-
* @private
|
|
1213
|
-
* @async
|
|
1214
|
-
* @returns {Promise<Object|null>}
|
|
1215
|
-
*/
|
|
1216
988
|
async _captureGeometry() {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
return {
|
|
1220
|
-
timestamp: Date.now(),
|
|
1221
|
-
// In production: serialize THREE.js scene to JSON or glTF
|
|
1222
|
-
};
|
|
989
|
+
// Placeholder: capture current 3D scene geometry
|
|
990
|
+
return {timestamp: Date.now(), meshCount: 0};
|
|
1223
991
|
},
|
|
1224
992
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
* @param {Object} project
|
|
1230
|
-
* @returns {number}
|
|
1231
|
-
*/
|
|
1232
|
-
_estimateSize(project) {
|
|
1233
|
-
return Object.values(project.files).reduce((sum, f) => sum + f.size, 0);
|
|
993
|
+
async _computeFileHash(arrayBuffer) {
|
|
994
|
+
const buffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
|
995
|
+
const hashArray = Array.from(new Uint8Array(buffer));
|
|
996
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
1234
997
|
},
|
|
1235
998
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
*
|
|
1239
|
-
* @private
|
|
1240
|
-
* @param {string} fileName
|
|
1241
|
-
* @returns {string} MIME type
|
|
1242
|
-
*/
|
|
1243
|
-
_detectFileType(fileName) {
|
|
1244
|
-
const ext = fileName.split('.').pop().toLowerCase();
|
|
999
|
+
_detectFileType(filename) {
|
|
1000
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
1245
1001
|
const types = {
|
|
1246
|
-
ipt: 'application/
|
|
1247
|
-
iam: 'application/
|
|
1248
|
-
step: 'application/step',
|
|
1249
|
-
stp: 'application/step',
|
|
1250
|
-
stl: '
|
|
1251
|
-
|
|
1252
|
-
|
|
1002
|
+
'ipt': 'application/inventor-part',
|
|
1003
|
+
'iam': 'application/inventor-assembly',
|
|
1004
|
+
'step': 'application/step',
|
|
1005
|
+
'stp': 'application/step',
|
|
1006
|
+
'stl': 'model/stl',
|
|
1007
|
+
'obj': 'model/obj',
|
|
1008
|
+
'dxf': 'application/dxf'
|
|
1253
1009
|
};
|
|
1254
1010
|
return types[ext] || 'application/octet-stream';
|
|
1255
1011
|
},
|
|
1256
1012
|
|
|
1257
|
-
/**
|
|
1258
|
-
* Load recent files from localStorage.
|
|
1259
|
-
*
|
|
1260
|
-
* @private
|
|
1261
|
-
*/
|
|
1262
|
-
_loadRecent() {
|
|
1263
|
-
const saved = localStorage.getItem('data_recent');
|
|
1264
|
-
if (saved) {
|
|
1265
|
-
this.state.recentFiles = JSON.parse(saved);
|
|
1266
|
-
}
|
|
1267
|
-
},
|
|
1268
|
-
|
|
1269
|
-
/**
|
|
1270
|
-
* Add file to recent list.
|
|
1271
|
-
*
|
|
1272
|
-
* @private
|
|
1273
|
-
* @param {Object} fileInfo
|
|
1274
|
-
*/
|
|
1275
|
-
_addToRecent(fileInfo) {
|
|
1276
|
-
// Remove if already exists
|
|
1277
|
-
this.state.recentFiles = this.state.recentFiles.filter(
|
|
1278
|
-
(f) => f.projectId !== fileInfo.projectId
|
|
1279
|
-
);
|
|
1280
|
-
|
|
1281
|
-
// Add to front
|
|
1282
|
-
this.state.recentFiles.unshift(fileInfo);
|
|
1283
|
-
|
|
1284
|
-
// Keep only 20
|
|
1285
|
-
this.state.recentFiles = this.state.recentFiles.slice(0, 20);
|
|
1286
|
-
|
|
1287
|
-
localStorage.setItem('data_recent', JSON.stringify(this.state.recentFiles));
|
|
1288
|
-
},
|
|
1289
|
-
|
|
1290
|
-
/**
|
|
1291
|
-
* Start auto-save interval.
|
|
1292
|
-
*
|
|
1293
|
-
* @private
|
|
1294
|
-
*/
|
|
1295
1013
|
_startAutoSave() {
|
|
1296
1014
|
this.state.autoSaveHandle = setInterval(() => {
|
|
1297
1015
|
if (this.state.currentProjectId) {
|
|
1298
|
-
this.save().catch(
|
|
1299
|
-
console.warn('Auto-save failed:', err);
|
|
1300
|
-
});
|
|
1016
|
+
this.save().catch(err => console.warn('[Data] Auto-save failed:', err));
|
|
1301
1017
|
}
|
|
1302
|
-
}, this.state.
|
|
1303
|
-
},
|
|
1304
|
-
|
|
1305
|
-
/**
|
|
1306
|
-
* Show notification.
|
|
1307
|
-
*
|
|
1308
|
-
* @private
|
|
1309
|
-
* @param {string} message
|
|
1310
|
-
* @param {string} type
|
|
1311
|
-
*/
|
|
1312
|
-
_showNotification(message, type = 'info') {
|
|
1313
|
-
const toast = document.createElement('div');
|
|
1314
|
-
toast.className = `data-toast data-toast-${type}`;
|
|
1315
|
-
toast.textContent = message;
|
|
1316
|
-
document.body.appendChild(toast);
|
|
1317
|
-
|
|
1318
|
-
setTimeout(() => toast.remove(), 4000);
|
|
1319
|
-
},
|
|
1320
|
-
|
|
1321
|
-
/**
|
|
1322
|
-
* Broadcast event.
|
|
1323
|
-
*
|
|
1324
|
-
* @private
|
|
1325
|
-
* @param {string} eventName
|
|
1326
|
-
* @param {Object} detail
|
|
1327
|
-
*/
|
|
1328
|
-
_broadcastEvent(eventName, detail) {
|
|
1329
|
-
const event = new CustomEvent(eventName, { detail });
|
|
1330
|
-
document.dispatchEvent(event);
|
|
1018
|
+
}, this.state.autoSaveFrequency);
|
|
1331
1019
|
},
|
|
1332
1020
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1021
|
+
_setupStorageQuotaMonitoring() {
|
|
1022
|
+
if (navigator.storage && navigator.storage.estimate) {
|
|
1023
|
+
setInterval(() => {
|
|
1024
|
+
navigator.storage.estimate().then(estimate => {
|
|
1025
|
+
this.state.usageBytes = estimate.usage;
|
|
1026
|
+
this.state.quotaBytes = estimate.quota;
|
|
1027
|
+
const percentUsed = (estimate.usage / estimate.quota) * 100;
|
|
1028
|
+
if (percentUsed > 90) {
|
|
1029
|
+
this._showNotification('Storage quota nearly full', 'warning');
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}, 60000);
|
|
1033
|
+
}
|
|
1345
1034
|
},
|
|
1346
1035
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
1355
|
-
let code = '';
|
|
1356
|
-
for (let i = 0; i < 8; i++) {
|
|
1357
|
-
code += chars[Math.floor(Math.random() * chars.length)];
|
|
1036
|
+
_detectAndRestoreCrash() {
|
|
1037
|
+
const crashFlag = localStorage.getItem('data_appRunning');
|
|
1038
|
+
if (crashFlag === 'true') {
|
|
1039
|
+
this._broadcastEvent('data:crashDetected', {});
|
|
1040
|
+
localStorage.removeItem('data_appRunning');
|
|
1041
|
+
} else {
|
|
1042
|
+
localStorage.setItem('data_appRunning', 'true');
|
|
1358
1043
|
}
|
|
1359
|
-
return code;
|
|
1360
1044
|
},
|
|
1361
1045
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
helpEntries: [
|
|
1367
|
-
{
|
|
1368
|
-
title: 'Create a New Project',
|
|
1369
|
-
description:
|
|
1370
|
-
'Click Data → New Project to start a new design. Give it a name and optionally a description.',
|
|
1371
|
-
category: 'Data Management',
|
|
1372
|
-
shortcut: null,
|
|
1373
|
-
},
|
|
1374
|
-
{
|
|
1375
|
-
title: 'Save Project',
|
|
1376
|
-
description:
|
|
1377
|
-
'Your project saves automatically every 30 seconds. You can also press Ctrl+S to save manually.',
|
|
1378
|
-
category: 'Data Management',
|
|
1379
|
-
shortcut: 'Ctrl+S',
|
|
1380
|
-
},
|
|
1381
|
-
{
|
|
1382
|
-
title: 'Import Files',
|
|
1383
|
-
description:
|
|
1384
|
-
'Click Data → Import File to add .ipt, .iam, .step, .stl, and other files to your project.',
|
|
1385
|
-
category: 'Data Management',
|
|
1386
|
-
shortcut: null,
|
|
1387
|
-
},
|
|
1388
|
-
{
|
|
1389
|
-
title: 'Create Share Link',
|
|
1390
|
-
description:
|
|
1391
|
-
'Click Data → Share to generate a link that teammates can use to view or edit your project without creating an account.',
|
|
1392
|
-
category: 'Data Management',
|
|
1393
|
-
shortcut: null,
|
|
1394
|
-
},
|
|
1395
|
-
{
|
|
1396
|
-
title: 'Export Project as ZIP',
|
|
1397
|
-
description:
|
|
1398
|
-
'Click Data → Export to save your entire project (with all files) as a ZIP archive for backup or sharing.',
|
|
1399
|
-
category: 'Data Management',
|
|
1400
|
-
shortcut: null,
|
|
1401
|
-
},
|
|
1402
|
-
{
|
|
1403
|
-
title: 'Create a Template',
|
|
1404
|
-
description:
|
|
1405
|
-
'Click Data → Save as Template to create a reusable template. You can then create new projects based on it.',
|
|
1406
|
-
category: 'Data Management',
|
|
1407
|
-
shortcut: null,
|
|
1408
|
-
},
|
|
1409
|
-
{
|
|
1410
|
-
title: 'Recover Deleted Projects',
|
|
1411
|
-
description:
|
|
1412
|
-
'Deleted projects go to trash for 30 days. Click Data → Trash to view and recover deleted projects.',
|
|
1413
|
-
category: 'Data Management',
|
|
1414
|
-
shortcut: null,
|
|
1415
|
-
},
|
|
1416
|
-
{
|
|
1417
|
-
title: 'Storage Quota',
|
|
1418
|
-
description:
|
|
1419
|
-
'Check how much storage you\'ve used. Click Data → Storage Info. You get 1GB free; Pro users get 100GB.',
|
|
1420
|
-
category: 'Data Management',
|
|
1421
|
-
shortcut: null,
|
|
1422
|
-
},
|
|
1423
|
-
],
|
|
1424
|
-
|
|
1425
|
-
// ========================================================================
|
|
1426
|
-
// UI PANEL — HTML and Styling
|
|
1427
|
-
// ========================================================================
|
|
1428
|
-
|
|
1429
|
-
/**
|
|
1430
|
-
* Get the HTML for the data management panel.
|
|
1431
|
-
*
|
|
1432
|
-
* @returns {string} HTML markup
|
|
1433
|
-
*/
|
|
1434
|
-
getUI() {
|
|
1435
|
-
return `
|
|
1436
|
-
<div class="data-panel" id="data-panel">
|
|
1437
|
-
<div class="data-header">
|
|
1438
|
-
<h3>Data & Projects</h3>
|
|
1439
|
-
<button class="data-close-btn" data-close-panel="data-panel">×</button>
|
|
1440
|
-
</div>
|
|
1441
|
-
|
|
1442
|
-
<div class="data-content">
|
|
1443
|
-
<div class="data-toolbar">
|
|
1444
|
-
<button id="data-new-btn" class="data-btn data-btn-primary">
|
|
1445
|
-
✚ New Project
|
|
1446
|
-
</button>
|
|
1447
|
-
<button id="data-open-btn" class="data-btn">
|
|
1448
|
-
📁 Open
|
|
1449
|
-
</button>
|
|
1450
|
-
</div>
|
|
1451
|
-
|
|
1452
|
-
<div class="data-tabs">
|
|
1453
|
-
<button class="data-tab active" data-tab="projects">Projects</button>
|
|
1454
|
-
<button class="data-tab" data-tab="recent">Recent</button>
|
|
1455
|
-
<button class="data-tab" data-tab="templates">Templates</button>
|
|
1456
|
-
</div>
|
|
1457
|
-
|
|
1458
|
-
<!-- Projects Tab -->
|
|
1459
|
-
<div id="data-projects" class="data-tab-content active">
|
|
1460
|
-
<div id="data-project-list" class="data-project-list"></div>
|
|
1461
|
-
</div>
|
|
1462
|
-
|
|
1463
|
-
<!-- Recent Tab -->
|
|
1464
|
-
<div id="data-recent" class="data-tab-content">
|
|
1465
|
-
<div id="data-recent-list" class="data-recent-list"></div>
|
|
1466
|
-
</div>
|
|
1467
|
-
|
|
1468
|
-
<!-- Templates Tab -->
|
|
1469
|
-
<div id="data-templates" class="data-tab-content">
|
|
1470
|
-
<div id="data-template-list" class="data-template-list"></div>
|
|
1471
|
-
</div>
|
|
1472
|
-
</div>
|
|
1473
|
-
</div>
|
|
1474
|
-
|
|
1475
|
-
<style>
|
|
1476
|
-
.data-panel {
|
|
1477
|
-
position: fixed;
|
|
1478
|
-
left: 0;
|
|
1479
|
-
top: 80px;
|
|
1480
|
-
width: 320px;
|
|
1481
|
-
height: 600px;
|
|
1482
|
-
background: #1e1e1e;
|
|
1483
|
-
border-right: 1px solid #333;
|
|
1484
|
-
display: flex;
|
|
1485
|
-
flex-direction: column;
|
|
1486
|
-
z-index: 1000;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
.data-header {
|
|
1490
|
-
display: flex;
|
|
1491
|
-
justify-content: space-between;
|
|
1492
|
-
align-items: center;
|
|
1493
|
-
padding: 12px;
|
|
1494
|
-
border-bottom: 1px solid #333;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
.data-header h3 {
|
|
1498
|
-
margin: 0;
|
|
1499
|
-
color: #e0e0e0;
|
|
1500
|
-
font-size: 14px;
|
|
1501
|
-
font-weight: 600;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
.data-close-btn {
|
|
1505
|
-
background: none;
|
|
1506
|
-
border: none;
|
|
1507
|
-
color: #999;
|
|
1508
|
-
font-size: 20px;
|
|
1509
|
-
cursor: pointer;
|
|
1510
|
-
padding: 0;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
.data-content {
|
|
1514
|
-
flex: 1;
|
|
1515
|
-
overflow: hidden;
|
|
1516
|
-
display: flex;
|
|
1517
|
-
flex-direction: column;
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
.data-toolbar {
|
|
1521
|
-
display: flex;
|
|
1522
|
-
gap: 6px;
|
|
1523
|
-
padding: 12px;
|
|
1524
|
-
border-bottom: 1px solid #333;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
.data-btn {
|
|
1528
|
-
flex: 1;
|
|
1529
|
-
padding: 8px;
|
|
1530
|
-
border: none;
|
|
1531
|
-
border-radius: 4px;
|
|
1532
|
-
background: #333;
|
|
1533
|
-
color: #e0e0e0;
|
|
1534
|
-
font-size: 12px;
|
|
1535
|
-
cursor: pointer;
|
|
1536
|
-
transition: background 0.2s;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
.data-btn:hover {
|
|
1540
|
-
background: #444;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
.data-btn-primary {
|
|
1544
|
-
background: #0284C7;
|
|
1545
|
-
color: white;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
.data-btn-primary:hover {
|
|
1549
|
-
background: #0369a1;
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
.data-tabs {
|
|
1553
|
-
display: flex;
|
|
1554
|
-
border-bottom: 1px solid #333;
|
|
1555
|
-
gap: 0;
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
.data-tab {
|
|
1559
|
-
flex: 1;
|
|
1560
|
-
padding: 8px;
|
|
1561
|
-
border: none;
|
|
1562
|
-
background: transparent;
|
|
1563
|
-
color: #999;
|
|
1564
|
-
font-size: 12px;
|
|
1565
|
-
cursor: pointer;
|
|
1566
|
-
border-bottom: 2px solid transparent;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
.data-tab.active {
|
|
1570
|
-
color: #0284C7;
|
|
1571
|
-
border-bottom-color: #0284C7;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
.data-tab-content {
|
|
1575
|
-
flex: 1;
|
|
1576
|
-
overflow-y: auto;
|
|
1577
|
-
padding: 12px;
|
|
1578
|
-
display: none;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
.data-tab-content.active {
|
|
1582
|
-
display: block;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
.data-project-list,
|
|
1586
|
-
.data-recent-list,
|
|
1587
|
-
.data-template-list {
|
|
1588
|
-
display: flex;
|
|
1589
|
-
flex-direction: column;
|
|
1590
|
-
gap: 8px;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
.data-item {
|
|
1594
|
-
padding: 8px;
|
|
1595
|
-
background: #2a2a2a;
|
|
1596
|
-
border-radius: 4px;
|
|
1597
|
-
cursor: pointer;
|
|
1598
|
-
transition: background 0.2s;
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
.data-item:hover {
|
|
1602
|
-
background: #333;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
.data-item-name {
|
|
1606
|
-
font-weight: 600;
|
|
1607
|
-
color: #e0e0e0;
|
|
1608
|
-
font-size: 12px;
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
.data-item-meta {
|
|
1612
|
-
color: #666;
|
|
1613
|
-
font-size: 10px;
|
|
1614
|
-
margin-top: 4px;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
.data-toast {
|
|
1618
|
-
position: fixed;
|
|
1619
|
-
bottom: 20px;
|
|
1620
|
-
right: 20px;
|
|
1621
|
-
padding: 12px 16px;
|
|
1622
|
-
border-radius: 4px;
|
|
1623
|
-
font-size: 12px;
|
|
1624
|
-
animation: slideInRight 0.3s ease;
|
|
1625
|
-
z-index: 10000;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
@keyframes slideInRight {
|
|
1629
|
-
from {
|
|
1630
|
-
transform: translateX(100%);
|
|
1631
|
-
opacity: 0;
|
|
1632
|
-
}
|
|
1633
|
-
to {
|
|
1634
|
-
transform: translateX(0);
|
|
1635
|
-
opacity: 1;
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
.data-toast-success {
|
|
1640
|
-
background: #1b5e20;
|
|
1641
|
-
color: #81c784;
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
.data-toast-error {
|
|
1645
|
-
background: #b71c1c;
|
|
1646
|
-
color: #ff5252;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
.data-toast-info {
|
|
1650
|
-
background: #01579b;
|
|
1651
|
-
color: #81d4fa;
|
|
1652
|
-
}
|
|
1653
|
-
</style>
|
|
1654
|
-
`;
|
|
1046
|
+
_showNotification(message, type) {
|
|
1047
|
+
console.log(`[Data] [${type.toUpperCase()}] ${message}`);
|
|
1048
|
+
this._broadcastEvent('data:notification', {message, type});
|
|
1655
1049
|
},
|
|
1050
|
+
|
|
1051
|
+
_broadcastEvent(eventName, data) {
|
|
1052
|
+
window.dispatchEvent(new CustomEvent(eventName, {detail: data}));
|
|
1053
|
+
}
|
|
1656
1054
|
};
|