You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

storage.ts 7.0KB

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