|
|
@@ -1,67 +1,190 @@
|
|
1
|
|
-import { Data, Page, PageState } from 'types'
|
|
|
1
|
+import * as fa from 'browser-fs-access'
|
|
|
2
|
+import { Data, Page, PageState, TLDocument } from 'types'
|
|
2
|
3
|
import { setToArray } from 'utils/utils'
|
|
|
4
|
+import state from './state'
|
|
|
5
|
+import { v4 as uuid } from 'uuid'
|
|
3
|
6
|
|
|
4
|
|
-const CURRENT_VERSION = 'code_slate_0.0.4'
|
|
|
7
|
+const CURRENT_VERSION = 'code_slate_0.0.5'
|
|
5
|
8
|
const DOCUMENT_ID = '0001'
|
|
6
|
9
|
|
|
7
|
|
-function storageId(label: string, id: string) {
|
|
8
|
|
- return `${CURRENT_VERSION}_doc_${DOCUMENT_ID}_${label}_${id}`
|
|
|
10
|
+function storageId(label: string, fileId: string, id: string) {
|
|
|
11
|
+ return `${CURRENT_VERSION}_doc_${fileId}_${label}_${id}`
|
|
9
|
12
|
}
|
|
10
|
13
|
|
|
11
|
14
|
class Storage {
|
|
12
|
|
- // Saving
|
|
13
|
|
- load(data: Data, id = CURRENT_VERSION) {
|
|
14
|
|
- if (typeof window === 'undefined') return
|
|
15
|
|
- if (typeof localStorage === 'undefined') return
|
|
|
15
|
+ load(data: Data, restoredData: any) {
|
|
|
16
|
+ // Empty shapes in state for each page
|
|
|
17
|
+ for (let key in restoredData.document.pages) {
|
|
|
18
|
+ restoredData.document.pages[key].shapes = {}
|
|
|
19
|
+ }
|
|
16
|
20
|
|
|
17
|
|
- // Load data from local storage
|
|
18
|
|
- const savedData = localStorage.getItem(id)
|
|
|
21
|
+ // Empty page states for each page
|
|
|
22
|
+ for (let key in restoredData.pageStates) {
|
|
|
23
|
+ restoredData.document.pages[key].shapes = {}
|
|
|
24
|
+ }
|
|
19
|
25
|
|
|
20
|
|
- if (savedData !== null) {
|
|
21
|
|
- const restoredData = JSON.parse(savedData)
|
|
|
26
|
+ data.document = {} as TLDocument
|
|
|
27
|
+ data.pageStates = {}
|
|
22
|
28
|
|
|
23
|
|
- // Empty shapes in state for each page
|
|
24
|
|
- for (let key in restoredData.document.pages) {
|
|
25
|
|
- restoredData.document.pages[key].shapes = {}
|
|
26
|
|
- }
|
|
|
29
|
+ // Merge restored data into state
|
|
|
30
|
+ Object.assign(data, restoredData)
|
|
27
|
31
|
|
|
28
|
|
- // Empty page states for each page
|
|
29
|
|
- for (let key in restoredData.pageStates) {
|
|
30
|
|
- restoredData.document.pages[key].shapes = {}
|
|
31
|
|
- }
|
|
|
32
|
+ // Minor migrtation: add id and name to document
|
|
|
33
|
+ data.document = {
|
|
|
34
|
+ id: 'document0',
|
|
|
35
|
+ name: 'My Document',
|
|
|
36
|
+ ...restoredData.document,
|
|
|
37
|
+ }
|
|
|
38
|
+
|
|
|
39
|
+ // Load current page
|
|
|
40
|
+ this.loadPage(data, data.currentPageId)
|
|
|
41
|
+ }
|
|
|
42
|
+
|
|
|
43
|
+ async loadDocumentFromFilesystem() {
|
|
|
44
|
+ const blob = await fa.fileOpen({
|
|
|
45
|
+ description: 'tldraw files',
|
|
|
46
|
+ })
|
|
32
|
47
|
|
|
33
|
|
- // Merge restored data into state
|
|
34
|
|
- Object.assign(data, restoredData)
|
|
|
48
|
+ const text = await getTextFromBlob(blob)
|
|
35
|
49
|
|
|
36
|
|
- // Load current page
|
|
37
|
|
- this.loadPage(data, data.currentPageId)
|
|
|
50
|
+ const restoredData = JSON.parse(text)
|
|
|
51
|
+
|
|
|
52
|
+ if (restoredData === null) {
|
|
|
53
|
+ console.warn('Could not load that data.')
|
|
|
54
|
+ return
|
|
38
|
55
|
}
|
|
|
56
|
+
|
|
|
57
|
+ state.send('LOADED_FROM_FILE', { restoredData })
|
|
39
|
58
|
}
|
|
40
|
59
|
|
|
41
|
|
- save = (data: Data, id = CURRENT_VERSION) => {
|
|
|
60
|
+ loadDocumentFromLocalStorage(data: Data, fileId = DOCUMENT_ID) {
|
|
42
|
61
|
if (typeof window === 'undefined') return
|
|
43
|
62
|
if (typeof localStorage === 'undefined') return
|
|
44
|
63
|
|
|
|
64
|
+ // Load data from local storage
|
|
|
65
|
+ const savedData = localStorage.getItem(fileId)
|
|
|
66
|
+
|
|
|
67
|
+ if (savedData === null) return false
|
|
|
68
|
+
|
|
|
69
|
+ const restoredData = JSON.parse(savedData)
|
|
|
70
|
+
|
|
|
71
|
+ this.load(data, restoredData)
|
|
|
72
|
+ }
|
|
|
73
|
+
|
|
|
74
|
+ getDataToSave = (data: Data) => {
|
|
45
|
75
|
const dataToSave: any = { ...data }
|
|
46
|
76
|
|
|
47
|
|
- // Don't save pageStates
|
|
|
77
|
+ for (let pageId in data.document) {
|
|
|
78
|
+ const savedPage = localStorage.getItem(
|
|
|
79
|
+ storageId(data.document.id, 'page', pageId)
|
|
|
80
|
+ )
|
|
|
81
|
+
|
|
|
82
|
+ if (savedPage !== null) {
|
|
|
83
|
+ const restored: Page = JSON.parse(savedPage)
|
|
|
84
|
+ dataToSave.document.pages[pageId] = restored
|
|
|
85
|
+ }
|
|
|
86
|
+ }
|
|
|
87
|
+
|
|
48
|
88
|
dataToSave.pageStates = {}
|
|
49
|
89
|
|
|
|
90
|
+ return JSON.stringify(dataToSave, null, 2)
|
|
|
91
|
+ }
|
|
|
92
|
+
|
|
|
93
|
+ saveToLocalStorage = (data: Data, id = data.document.id) => {
|
|
|
94
|
+ if (typeof window === 'undefined') return
|
|
|
95
|
+ if (typeof localStorage === 'undefined') return
|
|
|
96
|
+
|
|
50
|
97
|
// Save current data to local storage
|
|
51
|
|
- localStorage.setItem(id, JSON.stringify(dataToSave))
|
|
|
98
|
+ localStorage.setItem(id, this.getDataToSave(data))
|
|
|
99
|
+
|
|
|
100
|
+ // Save current page too
|
|
|
101
|
+ this.savePage(data, id, data.currentPageId)
|
|
|
102
|
+
|
|
|
103
|
+ state.send('SAVED_FILE_TO_LOCAL_STORAGE')
|
|
|
104
|
+ }
|
|
|
105
|
+
|
|
|
106
|
+ saveAsToFileSystem = (data: Data) => {
|
|
|
107
|
+ // Create a new document id when saving to the file system
|
|
|
108
|
+ this.saveToFileSystem(data, uuid())
|
|
|
109
|
+ }
|
|
|
110
|
+
|
|
|
111
|
+ saveToFileSystem = (data: Data, id = data.document.id) => {
|
|
|
112
|
+ // Save to local storage first
|
|
|
113
|
+ this.saveToLocalStorage(data, id)
|
|
|
114
|
+
|
|
|
115
|
+ const json = this.getDataToSave(data)
|
|
|
116
|
+
|
|
|
117
|
+ const blob = new Blob([json], {
|
|
|
118
|
+ type: 'application/vnd.tldraw+json',
|
|
|
119
|
+ })
|
|
|
120
|
+
|
|
|
121
|
+ fa.fileSave(blob, {
|
|
|
122
|
+ fileName: `${data.document.name}.tldr`,
|
|
|
123
|
+ description: 'tldraw file',
|
|
|
124
|
+ extensions: ['.tldr'],
|
|
|
125
|
+ })
|
|
|
126
|
+ .then(() => {
|
|
|
127
|
+ state.send('SAVED_FILE_TO_FILE_SYSTEM')
|
|
|
128
|
+ })
|
|
|
129
|
+ .catch((e) => {
|
|
|
130
|
+ state.send('CANCELLED_SAVE', { reason: e.message })
|
|
|
131
|
+ })
|
|
|
132
|
+ }
|
|
|
133
|
+
|
|
|
134
|
+ loadPageFromLocalStorage(fileId: string, pageId: string) {
|
|
|
135
|
+ let restored: Page
|
|
|
136
|
+
|
|
|
137
|
+ const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
|
|
|
138
|
+
|
|
|
139
|
+ if (savedPage !== null) {
|
|
|
140
|
+ restored = JSON.parse(savedPage)
|
|
|
141
|
+ } else {
|
|
|
142
|
+ restored = {
|
|
|
143
|
+ id: pageId,
|
|
|
144
|
+ type: 'page',
|
|
|
145
|
+ childIndex: 0,
|
|
|
146
|
+ name: 'Page',
|
|
|
147
|
+ shapes: {},
|
|
|
148
|
+ }
|
|
|
149
|
+ }
|
|
52
|
150
|
|
|
53
|
|
- // Save current page
|
|
54
|
|
- this.savePage(data, data.currentPageId)
|
|
|
151
|
+ return restored
|
|
55
|
152
|
}
|
|
56
|
153
|
|
|
57
|
|
- savePage(data: Data, pageId: string) {
|
|
|
154
|
+ loadPageStateFromLocalStorage(fileId: string, pageId: string) {
|
|
|
155
|
+ let restored: PageState
|
|
|
156
|
+
|
|
|
157
|
+ const savedPageState = localStorage.getItem(
|
|
|
158
|
+ storageId(fileId, 'pageState', pageId)
|
|
|
159
|
+ )
|
|
|
160
|
+
|
|
|
161
|
+ if (savedPageState !== null) {
|
|
|
162
|
+ restored = JSON.parse(savedPageState)
|
|
|
163
|
+ restored.selectedIds = new Set(restored.selectedIds)
|
|
|
164
|
+ } else {
|
|
|
165
|
+ restored = {
|
|
|
166
|
+ camera: {
|
|
|
167
|
+ point: [0, 0],
|
|
|
168
|
+ zoom: 1,
|
|
|
169
|
+ },
|
|
|
170
|
+ selectedIds: new Set([]),
|
|
|
171
|
+ }
|
|
|
172
|
+ }
|
|
|
173
|
+
|
|
|
174
|
+ return restored
|
|
|
175
|
+ }
|
|
|
176
|
+
|
|
|
177
|
+ savePage(data: Data, fileId = data.document.id, pageId = data.currentPageId) {
|
|
58
|
178
|
if (typeof window === 'undefined') return
|
|
59
|
179
|
if (typeof localStorage === 'undefined') return
|
|
60
|
180
|
|
|
61
|
181
|
// Save page
|
|
62
|
182
|
const page = data.document.pages[pageId]
|
|
63
|
183
|
|
|
64
|
|
- localStorage.setItem(storageId('page', pageId), JSON.stringify(page))
|
|
|
184
|
+ localStorage.setItem(
|
|
|
185
|
+ storageId(fileId, 'page', pageId),
|
|
|
186
|
+ JSON.stringify(page)
|
|
|
187
|
+ )
|
|
65
|
188
|
|
|
66
|
189
|
// Save page state
|
|
67
|
190
|
|
|
|
@@ -80,39 +203,20 @@ class Storage {
|
|
80
|
203
|
}
|
|
81
|
204
|
|
|
82
|
205
|
localStorage.setItem(
|
|
83
|
|
- storageId('pageState', pageId),
|
|
|
206
|
+ storageId(fileId, 'pageState', pageId),
|
|
84
|
207
|
JSON.stringify(pageState)
|
|
85
|
208
|
)
|
|
86
|
209
|
}
|
|
87
|
210
|
|
|
88
|
|
- loadPage(data: Data, pageId: string) {
|
|
|
211
|
+ loadPage(data: Data, pageId = data.currentPageId) {
|
|
89
|
212
|
if (typeof window === 'undefined') return
|
|
90
|
213
|
if (typeof localStorage === 'undefined') return
|
|
91
|
214
|
|
|
92
|
|
- // Load page and merge into state
|
|
93
|
|
- const savedPage = localStorage.getItem(storageId('page', pageId))
|
|
94
|
|
-
|
|
95
|
|
- if (savedPage !== null) {
|
|
96
|
|
- const restored: Page = JSON.parse(savedPage)
|
|
97
|
|
- data.document.pages[pageId] = restored
|
|
98
|
|
- }
|
|
|
215
|
+ const fileId = data.document.id
|
|
99
|
216
|
|
|
100
|
|
- // Load page state and merge into state
|
|
101
|
|
- const savedPageState = localStorage.getItem(storageId('pageState', pageId))
|
|
|
217
|
+ data.document.pages[pageId] = this.loadPageFromLocalStorage(fileId, pageId)
|
|
102
|
218
|
|
|
103
|
|
- if (savedPageState !== null) {
|
|
104
|
|
- const restored: PageState = JSON.parse(savedPageState)
|
|
105
|
|
- restored.selectedIds = new Set(restored.selectedIds)
|
|
106
|
|
- data.pageStates[pageId] = restored
|
|
107
|
|
- } else {
|
|
108
|
|
- data.pageStates[pageId] = {
|
|
109
|
|
- camera: {
|
|
110
|
|
- point: [0, 0],
|
|
111
|
|
- zoom: 1,
|
|
112
|
|
- },
|
|
113
|
|
- selectedIds: new Set([]),
|
|
114
|
|
- }
|
|
115
|
|
- }
|
|
|
219
|
+ data.pageStates[pageId] = this.loadPageStateFromLocalStorage(fileId, pageId)
|
|
116
|
220
|
|
|
117
|
221
|
// Empty shapes in state for other pages
|
|
118
|
222
|
for (let key in data.document.pages) {
|
|
|
@@ -120,13 +224,7 @@ class Storage {
|
|
120
|
224
|
data.document.pages[key].shapes = {}
|
|
121
|
225
|
}
|
|
122
|
226
|
|
|
123
|
|
- // Empty page states for other pages
|
|
124
|
|
- for (let key in data.pageStates) {
|
|
125
|
|
- if (key === pageId) continue
|
|
126
|
|
- data.document.pages[key].shapes = {}
|
|
127
|
|
- }
|
|
128
|
|
-
|
|
129
|
|
- // Update camera
|
|
|
227
|
+ // Update camera for the new page state
|
|
130
|
228
|
document.documentElement.style.setProperty(
|
|
131
|
229
|
'--camera-zoom',
|
|
132
|
230
|
data.pageStates[data.currentPageId].camera.zoom.toString()
|
|
|
@@ -137,3 +235,21 @@ class Storage {
|
|
137
|
235
|
const storage = new Storage()
|
|
138
|
236
|
|
|
139
|
237
|
export default storage
|
|
|
238
|
+
|
|
|
239
|
+async function getTextFromBlob(blob: Blob): Promise<string> {
|
|
|
240
|
+ // Return blob as text if a text file.
|
|
|
241
|
+ if ('text' in Blob) return blob.text()
|
|
|
242
|
+
|
|
|
243
|
+ // Return blob as text if a text file.
|
|
|
244
|
+ return new Promise((resolve) => {
|
|
|
245
|
+ const reader = new FileReader()
|
|
|
246
|
+
|
|
|
247
|
+ reader.onloadend = () => {
|
|
|
248
|
+ if (reader.readyState === FileReader.DONE) {
|
|
|
249
|
+ resolve(reader.result as string)
|
|
|
250
|
+ }
|
|
|
251
|
+ }
|
|
|
252
|
+
|
|
|
253
|
+ reader.readAsText(blob, 'utf8')
|
|
|
254
|
+ })
|
|
|
255
|
+}
|