選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

storage.ts 7.5KB

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