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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import * as fa from 'browser-fs-access'
  2. import { Data, Page, PageState, TLDocument } from 'types'
  3. import { lzw_decode, lzw_encode, 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.6'
  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(lzw_decode(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(lzw_decode(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(
  83. storageId(fileId, 'document', fileId),
  84. lzw_encode(dataToSave)
  85. )
  86. }
  87. loadDocumentFromJson(data: Data, restoredData: any) {
  88. this.load(data, restoredData)
  89. this.loadPage(data, data.currentPageId)
  90. this.saveToLocalStorage(data, data.document.id)
  91. localStorage.setItem(`${CURRENT_VERSION}_lastOpened`, data.document.id)
  92. }
  93. /* ---------------------- Pages --------------------- */
  94. async loadPreviousHandle() {
  95. const handle: fa.FileSystemHandle | undefined = await idb.get(
  96. 'previous_handle'
  97. )
  98. if (handle !== undefined) {
  99. this.previousSaveHandle = handle
  100. }
  101. }
  102. savePage(data: Data, fileId = data.document.id, pageId = data.currentPageId) {
  103. if (typeof window === 'undefined') return
  104. if (typeof localStorage === 'undefined') return
  105. // Save page
  106. const page = data.document.pages[pageId]
  107. const json = JSON.stringify(page)
  108. localStorage.setItem(storageId(fileId, 'page', pageId), lzw_encode(json))
  109. // Save page state
  110. let currentPageState = {
  111. camera: {
  112. point: [0, 0],
  113. zoom: 1,
  114. },
  115. selectedIds: new Set([]),
  116. ...data.pageStates[pageId],
  117. }
  118. localStorage.setItem(
  119. storageId(fileId, 'pageState', pageId),
  120. JSON.stringify({
  121. ...currentPageState,
  122. selectedIds: setToArray(currentPageState.selectedIds),
  123. })
  124. )
  125. }
  126. loadPage(data: Data, pageId = data.currentPageId) {
  127. if (typeof window === 'undefined') return
  128. if (typeof localStorage === 'undefined') return
  129. const fileId = data.document.id
  130. // Page
  131. const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
  132. if (savedPage !== null) {
  133. data.document.pages[pageId] = JSON.parse(lzw_decode(savedPage))
  134. } else {
  135. data.document.pages[pageId] = {
  136. id: pageId,
  137. type: 'page',
  138. childIndex: 0,
  139. name: 'Page',
  140. shapes: {},
  141. }
  142. }
  143. // Page state
  144. const savedPageState = localStorage.getItem(
  145. storageId(fileId, 'pageState', pageId)
  146. )
  147. if (savedPageState !== null) {
  148. const restored: PageState = JSON.parse(savedPageState)
  149. data.pageStates[pageId] = restored
  150. } else {
  151. data.pageStates[pageId] = {
  152. camera: {
  153. point: [0, 0],
  154. zoom: 1,
  155. },
  156. selectedIds: new Set([]),
  157. }
  158. }
  159. data.pageStates[pageId].selectedIds = new Set(
  160. data.pageStates[pageId].selectedIds
  161. )
  162. // Empty shapes in state for other pages
  163. for (let key in data.document.pages) {
  164. if (key === pageId) continue
  165. data.document.pages[key].shapes = {}
  166. }
  167. // Update camera for the new page state
  168. document.documentElement.style.setProperty(
  169. '--camera-zoom',
  170. data.pageStates[data.currentPageId].camera.zoom.toString()
  171. )
  172. }
  173. /* ------------------- File System ------------------ */
  174. saveToFileSystem = (data: Data) => {
  175. this.saveDataToFileSystem(data, data.document.id, false)
  176. }
  177. saveAsToFileSystem = (data: Data) => {
  178. this.saveDataToFileSystem(data, uuid(), true)
  179. }
  180. saveDataToFileSystem = (data: Data, fileId: string, saveAs: boolean) => {
  181. const json = this.getDataToSave(data)
  182. this.saveToLocalStorage(data, fileId)
  183. const blob = new Blob([json], {
  184. type: 'application/vnd.tldraw+json',
  185. })
  186. fa.fileSave(
  187. blob,
  188. {
  189. fileName: `${
  190. saveAs
  191. ? data.document.name
  192. : this.previousSaveHandle?.name || 'My Document'
  193. }.tldr`,
  194. description: 'tldraw file',
  195. extensions: ['.tldr'],
  196. },
  197. saveAs ? undefined : this.previousSaveHandle,
  198. true
  199. )
  200. .then((handle) => {
  201. this.previousSaveHandle = handle
  202. state.send('SAVED_FILE_TO_FILE_SYSTEM')
  203. idb.set('previous_handle', handle)
  204. })
  205. .catch((e) => {
  206. state.send('CANCELLED_SAVE', { reason: e.message })
  207. })
  208. }
  209. loadDocumentFromFilesystem() {
  210. console.warn('Loading file from file system.')
  211. fa.fileOpen({
  212. description: 'tldraw files',
  213. })
  214. .then((blob) =>
  215. getTextFromBlob(blob).then((text) => {
  216. const restoredData = JSON.parse(text)
  217. if (restoredData === null) {
  218. console.warn('Could not load that data.')
  219. return
  220. }
  221. // Save blob for future saves
  222. this.previousSaveHandle = blob.handle
  223. state.send('LOADED_FROM_FILE', { restoredData: { ...restoredData } })
  224. })
  225. )
  226. .catch((e) => {
  227. state.send('CANCELLED_SAVE', { reason: e.message })
  228. })
  229. }
  230. }
  231. const storage = new Storage()
  232. export default storage
  233. async function getTextFromBlob(blob: Blob): Promise<string> {
  234. // Return blob as text if a text file.
  235. if ('text' in Blob) return blob.text()
  236. // Return blob as text if a text file.
  237. return new Promise((resolve) => {
  238. const reader = new FileReader()
  239. reader.onloadend = () => {
  240. if (reader.readyState === FileReader.DONE) {
  241. resolve(reader.result as string)
  242. }
  243. }
  244. reader.readAsText(blob, 'utf8')
  245. })
  246. }