Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

index.tsx 4.8KB

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