index.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import type { FC } from 'react'
  2. import { useCallback, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import type { Area } from 'react-easy-crop'
  5. import Modal from '../modal'
  6. import Divider from '../divider'
  7. import Button from '../button'
  8. import { ImagePlus } from '../icons/src/vender/line/images'
  9. import { useLocalFileUploader } from '../image-uploader/hooks'
  10. import EmojiPickerInner from '../emoji-picker/Inner'
  11. import Uploader from './Uploader'
  12. import s from './style.module.css'
  13. import getCroppedImg from './utils'
  14. import type { AppIconType, ImageFile } from '@/types/app'
  15. import cn from '@/utils/classnames'
  16. import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
  17. export type AppIconEmojiSelection = {
  18. type: 'emoji'
  19. icon: string
  20. background: string
  21. }
  22. export type AppIconImageSelection = {
  23. type: 'image'
  24. fileId: string
  25. url: string
  26. }
  27. export type AppIconSelection = AppIconEmojiSelection | AppIconImageSelection
  28. type AppIconPickerProps = {
  29. onSelect?: (payload: AppIconSelection) => void
  30. onClose?: () => void
  31. className?: string
  32. }
  33. const AppIconPicker: FC<AppIconPickerProps> = ({
  34. onSelect,
  35. onClose,
  36. className,
  37. }) => {
  38. const { t } = useTranslation()
  39. const tabs = [
  40. { key: 'emoji', label: t('app.iconPicker.emoji'), icon: <span className="text-lg">🤖</span> },
  41. { key: 'image', label: t('app.iconPicker.image'), icon: <ImagePlus /> },
  42. ]
  43. const [activeTab, setActiveTab] = useState<AppIconType>('emoji')
  44. const [emoji, setEmoji] = useState<{ emoji: string; background: string }>()
  45. const handleSelectEmoji = useCallback((emoji: string, background: string) => {
  46. setEmoji({ emoji, background })
  47. }, [setEmoji])
  48. const [uploading, setUploading] = useState<boolean>()
  49. const { handleLocalFileUpload } = useLocalFileUploader({
  50. limit: 3,
  51. disabled: false,
  52. onUpload: (imageFile: ImageFile) => {
  53. if (imageFile.fileId) {
  54. setUploading(false)
  55. onSelect?.({
  56. type: 'image',
  57. fileId: imageFile.fileId,
  58. url: imageFile.url,
  59. })
  60. }
  61. },
  62. })
  63. const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string; croppedAreaPixels: Area; fileName: string }>()
  64. const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => {
  65. setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
  66. }
  67. const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
  68. const handleUpload = async (file?: File) => {
  69. setUploadImageInfo({ file })
  70. }
  71. const handleSelect = async () => {
  72. if (activeTab === 'emoji') {
  73. if (emoji) {
  74. onSelect?.({
  75. type: 'emoji',
  76. icon: emoji.emoji,
  77. background: emoji.background,
  78. })
  79. }
  80. }
  81. else {
  82. if (!imageCropInfo && !uploadImageInfo)
  83. return
  84. setUploading(true)
  85. if (imageCropInfo.file) {
  86. handleLocalFileUpload(imageCropInfo.file)
  87. return
  88. }
  89. const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
  90. const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
  91. handleLocalFileUpload(file)
  92. }
  93. }
  94. return <Modal
  95. onClose={() => { }}
  96. isShow
  97. closable={false}
  98. wrapperClassName={className}
  99. className={cn(s.container, '!w-[362px] !p-0')}
  100. >
  101. {!DISABLE_UPLOAD_IMAGE_AS_ICON && <div className="p-2 pb-0 w-full">
  102. <div className='p-1 flex items-center justify-center gap-2 bg-background-body rounded-xl'>
  103. {tabs.map(tab => (
  104. <button
  105. key={tab.key}
  106. className={`
  107. p-2 flex-1 flex justify-center items-center h-8 rounded-xl text-sm shrink-0 font-medium
  108. ${activeTab === tab.key && 'bg-components-main-nav-nav-button-bg-active shadow-md'}
  109. `}
  110. onClick={() => setActiveTab(tab.key as AppIconType)}
  111. >
  112. {tab.icon} &nbsp; {tab.label}
  113. </button>
  114. ))}
  115. </div>
  116. </div>}
  117. <Divider className='m-0' />
  118. <EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} />
  119. <Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
  120. <Divider className='m-0' />
  121. <div className='w-full flex items-center justify-center p-3 gap-2'>
  122. <Button className='w-full' onClick={() => onClose?.()}>
  123. {t('app.iconPicker.cancel')}
  124. </Button>
  125. <Button variant="primary" className='w-full' disabled={uploading} loading={uploading} onClick={handleSelect}>
  126. {t('app.iconPicker.ok')}
  127. </Button>
  128. </div>
  129. </Modal>
  130. }
  131. export default AppIconPicker