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 6.8KB

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