Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

storage.ts 8.0KB

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