您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

storage.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import * as fa from 'browser-fs-access'
  2. import { Data, PageState, TLDocument } from 'types'
  3. import { decompress, compress, setToArray } from 'utils/utils'
  4. import state from './state'
  5. import { uniqueId } from 'utils/utils'
  6. import * as idb from 'idb-keyval'
  7. const CURRENT_VERSION = 'code_slate_0.0.8'
  8. function storageId(fileId: string, label: string, id?: string) {
  9. return [CURRENT_VERSION, fileId, label, id].filter(Boolean).join('_')
  10. }
  11. class Storage {
  12. previousSaveHandle?: fa.FileSystemHandle
  13. constructor() {
  14. // this.loadPreviousHandle() // Still needs debugging
  15. }
  16. firstLoad(data: Data) {
  17. const lastOpenedFileId = localStorage.getItem(
  18. `${CURRENT_VERSION}_lastOpened`
  19. )
  20. // 1. Load Document from Local Storage
  21. // Using the "last opened file id" in local storage.
  22. if (lastOpenedFileId !== null) {
  23. // Load document from local storage
  24. const savedDocument = localStorage.getItem(
  25. storageId(lastOpenedFileId, 'document', lastOpenedFileId)
  26. )
  27. if (savedDocument === null) {
  28. // If no document found, create a fresh random id.
  29. data.document.id = uniqueId()
  30. } else {
  31. // If we did find a document, load it into state.
  32. const restoredDocument: TLDocument = JSON.parse(
  33. decompress(savedDocument)
  34. )
  35. // Merge restored data into state.
  36. data.document = restoredDocument
  37. }
  38. }
  39. try {
  40. this.load(data)
  41. } catch (error) {
  42. console.error(error)
  43. }
  44. }
  45. saveDocumentToLocalStorage(data: Data) {
  46. const document = this.getCompleteDocument(data)
  47. localStorage.setItem(
  48. storageId(data.document.id, 'document', data.document.id),
  49. compress(JSON.stringify(document))
  50. )
  51. }
  52. getCompleteDocument = (data: Data) => {
  53. // Create a safely mutable copy of the data
  54. const document: TLDocument = { ...data.document }
  55. // Try to find the document's pages and page states in local storage.
  56. Object.keys(document.pages).forEach((pageId) => {
  57. const savedPage = localStorage.getItem(
  58. storageId(document.id, 'page', pageId)
  59. )
  60. if (savedPage !== null) {
  61. document.pages[pageId] = JSON.parse(decompress(savedPage))
  62. }
  63. })
  64. return document
  65. }
  66. savePageState = (data: Data) => {
  67. localStorage.setItem(
  68. storageId(data.document.id, 'lastPageState', data.document.id),
  69. JSON.stringify(data.pageStates[data.currentPageId])
  70. )
  71. }
  72. loadDocumentFromJson(data: Data, json: string) {
  73. const restoredDocument: { document: TLDocument; pageState: PageState } =
  74. JSON.parse(json)
  75. data.document = restoredDocument.document
  76. // Save pages to local storage, possibly overwriting unsaved local copies
  77. Object.values(data.document.pages).forEach((page) => {
  78. localStorage.setItem(
  79. storageId(data.document.id, 'page', page.id),
  80. compress(JSON.stringify(page))
  81. )
  82. })
  83. localStorage.setItem(
  84. storageId(data.document.id, 'lastPageState', data.document.id),
  85. JSON.stringify(restoredDocument.pageState)
  86. )
  87. // Save the new file as the last opened document id
  88. localStorage.setItem(`${CURRENT_VERSION}_lastOpened`, data.document.id)
  89. this.load(data)
  90. }
  91. load(data: Data) {
  92. // Once we've loaded data either from local storage or json, run through these steps.
  93. data.pageStates = {}
  94. // 2. Load Pages from Local Storage
  95. // Try to find the document's pages and page states in local storage.
  96. Object.keys(data.document.pages).forEach((pageId) => {
  97. const savedPage = localStorage.getItem(
  98. storageId(data.document.id, 'page', pageId)
  99. )
  100. if (savedPage !== null) {
  101. // If we've found a page in local storage, set it into state.
  102. data.document.pages[pageId] = JSON.parse(decompress(savedPage))
  103. }
  104. const savedPageState = localStorage.getItem(
  105. storageId(data.document.id, 'pageState', pageId)
  106. )
  107. if (savedPageState !== null) {
  108. // If we've found a page state in local storage, set it into state.
  109. data.pageStates[pageId] = JSON.parse(decompress(savedPageState))
  110. data.pageStates[pageId].selectedIds = new Set([])
  111. } else {
  112. // Or else create a new one.
  113. data.pageStates[pageId] = {
  114. id: pageId,
  115. selectedIds: new Set([]),
  116. camera: {
  117. point: [0, 0],
  118. zoom: 1,
  119. },
  120. }
  121. }
  122. })
  123. // 3. Restore the last page state
  124. // Using the "last page state" in local storage.
  125. const savedPageState = localStorage.getItem(
  126. storageId(data.document.id, 'lastPageState', data.document.id)
  127. )
  128. if (savedPageState !== null) {
  129. const pageState = JSON.parse(decompress(savedPageState))
  130. pageState.selectedIds = new Set([])
  131. data.pageStates[pageState.id] = pageState
  132. data.currentPageId = pageState.id
  133. }
  134. // 4. Save the current document
  135. // The document is now "full" and ready. Whether we've restored a
  136. // document or created a new one, save the entire current document.
  137. localStorage.setItem(
  138. storageId(data.document.id, 'document', data.document.id),
  139. compress(JSON.stringify(data.document))
  140. )
  141. // 4.1
  142. // Also save out copies of each page separately.
  143. Object.values(data.document.pages).forEach((page) => {
  144. // Save page
  145. localStorage.setItem(
  146. storageId(data.document.id, 'page', page.id),
  147. compress(JSON.stringify(page))
  148. )
  149. })
  150. // Save the last page state
  151. const currentPageState = data.pageStates[data.currentPageId]
  152. localStorage.setItem(
  153. storageId(data.document.id, 'lastPageState', data.document.id),
  154. JSON.stringify(currentPageState)
  155. )
  156. // Finally, save the current document id as the "last opened" document id.
  157. localStorage.setItem(`${CURRENT_VERSION}_lastOpened`, data.document.id)
  158. // 5. Prepare the new state.
  159. // Clear out the other pages from state.
  160. Object.values(data.document.pages).forEach((page) => {
  161. if (page.id !== data.currentPageId) {
  162. page.shapes = {}
  163. }
  164. })
  165. // Update camera for the new page state
  166. document.documentElement.style.setProperty(
  167. '--camera-zoom',
  168. data.pageStates[data.currentPageId].camera.zoom.toString()
  169. )
  170. }
  171. /* ---------------------- Pages --------------------- */
  172. async loadPreviousHandle() {
  173. const handle: fa.FileSystemHandle | undefined = await idb.get(
  174. 'previous_handle'
  175. )
  176. if (handle !== undefined) {
  177. this.previousSaveHandle = handle
  178. }
  179. }
  180. savePage(data: Data, fileId = data.document.id, pageId = data.currentPageId) {
  181. const page = data.document.pages[pageId]
  182. // Save page
  183. localStorage.setItem(
  184. storageId(fileId, 'page', pageId),
  185. compress(JSON.stringify(page))
  186. )
  187. // Save page state
  188. let currentPageState = data.pageStates[pageId]
  189. localStorage.setItem(
  190. storageId(fileId, 'pageState', pageId),
  191. JSON.stringify({
  192. ...currentPageState,
  193. selectedIds: setToArray(currentPageState.selectedIds),
  194. })
  195. )
  196. }
  197. loadPage(data: Data, fileId = data.document.id, pageId = data.currentPageId) {
  198. if (typeof window === 'undefined') return
  199. if (typeof localStorage === 'undefined') return
  200. data.currentPageId = pageId
  201. // Get saved page from local storage
  202. const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
  203. if (savedPage !== null) {
  204. // If we have a page, move it into state
  205. data.document.pages[pageId] = JSON.parse(decompress(savedPage))
  206. } else {
  207. // If we don't have a page, create a new page
  208. data.document.pages[pageId] = {
  209. id: pageId,
  210. type: 'page',
  211. childIndex: Object.keys(data.document.pages).length,
  212. name: 'New Page',
  213. shapes: {},
  214. }
  215. }
  216. // Get saved page state from local storage
  217. const savedPageState = localStorage.getItem(
  218. storageId(fileId, 'pageState', pageId)
  219. )
  220. if (savedPageState !== null) {
  221. // If we have a page, move it into state
  222. const restored: PageState = JSON.parse(savedPageState)
  223. data.pageStates[pageId] = restored
  224. data.pageStates[pageId].selectedIds = new Set(restored.selectedIds)
  225. } else {
  226. data.pageStates[pageId] = {
  227. id: pageId,
  228. camera: {
  229. point: [0, 0],
  230. zoom: 1,
  231. },
  232. selectedIds: new Set([]),
  233. }
  234. }
  235. // Save the last page state
  236. localStorage.setItem(
  237. storageId(fileId, 'lastPageState'),
  238. JSON.stringify(data.pageStates[pageId])
  239. )
  240. // Prepare new state
  241. // Now clear out the other pages from state.
  242. Object.values(data.document.pages).forEach((page) => {
  243. if (page.id !== data.currentPageId) {
  244. page.shapes = {}
  245. }
  246. })
  247. // Update camera for the new page state
  248. document.documentElement.style.setProperty(
  249. '--camera-zoom',
  250. data.pageStates[data.currentPageId].camera.zoom.toString()
  251. )
  252. }
  253. /* ------------------- File System ------------------ */
  254. saveToFileSystem = (data: Data) => {
  255. this.saveDocumentToLocalStorage(data)
  256. this.saveDataToFileSystem(data, data.document.id, false)
  257. }
  258. saveAsToFileSystem = (data: Data) => {
  259. this.saveDocumentToLocalStorage(data)
  260. this.saveDataToFileSystem(data, uniqueId(), true)
  261. }
  262. saveDataToFileSystem = (data: Data, fileId: string, saveAs: boolean) => {
  263. const document = this.getCompleteDocument(data)
  264. // Then save to file system
  265. const blob = new Blob(
  266. [
  267. compress(
  268. JSON.stringify({
  269. document,
  270. pageState: data.pageStates[data.currentPageId],
  271. })
  272. ),
  273. ],
  274. {
  275. type: 'application/vnd.tldraw+json',
  276. }
  277. )
  278. fa.fileSave(
  279. blob,
  280. {
  281. fileName: `${
  282. saveAs
  283. ? data.document.name
  284. : this.previousSaveHandle?.name || 'My Document'
  285. }.tldr`,
  286. description: 'tldraw file',
  287. extensions: ['.tldr'],
  288. },
  289. saveAs ? undefined : this.previousSaveHandle,
  290. true
  291. )
  292. .then((handle) => {
  293. this.previousSaveHandle = handle
  294. state.send('SAVED_FILE_TO_FILE_SYSTEM')
  295. idb.set('previous_handle', handle)
  296. })
  297. .catch((e) => {
  298. state.send('CANCELLED_SAVE', { reason: e.message })
  299. })
  300. }
  301. loadDocumentFromFilesystem() {
  302. fa.fileOpen({
  303. description: 'tldraw files',
  304. })
  305. .then((blob) =>
  306. getTextFromBlob(blob).then((json) => {
  307. // Save blob for future saves
  308. this.previousSaveHandle = blob.handle
  309. state.send('LOADED_FROM_FILE', { json: decompress(json) })
  310. })
  311. )
  312. .catch((e) => {
  313. state.send('CANCELLED_SAVE', { reason: e.message })
  314. })
  315. }
  316. }
  317. const storage = new Storage()
  318. export default storage
  319. async function getTextFromBlob(blob: Blob): Promise<string> {
  320. // Return blob as text if a text file.
  321. if ('text' in Blob) return blob.text()
  322. // Return blob as text if a text file.
  323. return new Promise((resolve) => {
  324. const reader = new FileReader()
  325. reader.onloadend = () => {
  326. if (reader.readyState === FileReader.DONE) {
  327. resolve(reader.result as string)
  328. }
  329. }
  330. reader.readAsText(blob, 'utf8')
  331. })
  332. }