usePaste.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import {
  2. BoundsUtils,
  3. fileToBase64,
  4. getSizeFromSrc,
  5. TLAsset,
  6. TLShapeModel,
  7. uniqueId,
  8. } from '@tldraw/core'
  9. import type { TLReactCallbacks } from '@tldraw/react'
  10. import * as React from 'react'
  11. import type { Shape } from '~lib'
  12. export function usePaste() {
  13. return React.useCallback<TLReactCallbacks<Shape>['onFileDrop']>(async (app, { point }) => {
  14. const assetId = uniqueId()
  15. interface ImageAsset extends TLAsset {
  16. size: number[]
  17. }
  18. const assetsToCreate: ImageAsset[] = []
  19. const shapesToCreate: TLShapeModel[] = []
  20. async function handleImage(item: ClipboardItem) {
  21. const firstImageType = item.types.find(type => type.startsWith('image'))
  22. if (firstImageType) {
  23. const blob = await item.getType(firstImageType)
  24. const dataurl = await fileToBase64(blob)
  25. if (typeof dataurl !== 'string') return false
  26. const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl)
  27. if (existingAsset) {
  28. assetsToCreate.push(existingAsset as ImageAsset)
  29. return false
  30. }
  31. // Create a new asset for this image
  32. const asset: ImageAsset = {
  33. id: assetId,
  34. type: 'image',
  35. src: dataurl,
  36. size: await getSizeFromSrc(dataurl),
  37. }
  38. assetsToCreate.push(asset)
  39. return true
  40. }
  41. return false
  42. }
  43. async function handleLogseqShapes(item: ClipboardItem) {
  44. const plainTextType = item.types.find(type => type.startsWith('text/plain'))
  45. if (plainTextType) {
  46. const blob = await item.getType(plainTextType)
  47. const rawText = await blob.text()
  48. const data = JSON.parse(rawText)
  49. if (data.type === 'logseq/whiteboard-shapes') {
  50. const shapes = data.shapes as TLShapeModel[]
  51. const commonBounds = BoundsUtils.getCommonBounds(
  52. shapes.map(shape => ({
  53. minX: shape.point?.[0] ?? point[0],
  54. minY: shape.point?.[1] ?? point[1],
  55. width: shape.size?.[0] ?? 4,
  56. height: shape.size?.[1] ?? 4,
  57. maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
  58. maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
  59. }))
  60. )
  61. const clonedShape = data.shapes.map((shape: TLShapeModel) => {
  62. return {
  63. ...shape,
  64. handles: {}, // TODO: may add this later?
  65. id: uniqueId(),
  66. parentId: app.currentPageId,
  67. point: [
  68. point[0] + shape.point![0] - commonBounds.minX,
  69. point[1] + shape.point![1] - commonBounds.minY,
  70. ],
  71. }
  72. })
  73. shapesToCreate.push(...clonedShape)
  74. }
  75. }
  76. }
  77. // TODO: supporting other pasting formats
  78. for (const item of await navigator.clipboard.read()) {
  79. try {
  80. let handled = await handleImage(item)
  81. if (!handled) {
  82. await handleLogseqShapes(item)
  83. }
  84. } catch (error) {
  85. console.error(error)
  86. }
  87. }
  88. const allShapesToAdd = [
  89. ...assetsToCreate.map((asset, i) => ({
  90. id: uniqueId(),
  91. type: 'image',
  92. parentId: app.currentPageId,
  93. point: [point[0] - asset.size[0] / 2 + i * 16, point[1] - asset.size[1] / 2 + i * 16],
  94. size: asset.size,
  95. assetId: asset.id,
  96. opacity: 1,
  97. })),
  98. ...shapesToCreate,
  99. ]
  100. app.createAssets(assetsToCreate)
  101. app.createShapes(allShapesToAdd)
  102. app.setSelectedShapes(allShapesToAdd.map(s => s.id))
  103. }, [])
  104. }