mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 17:59:09 +08:00
feat: open message text file attachment in preview (#9644)
* feat: add text file preview (#7023) * feat: open message text file attachment in preview * refractor: use `window.api.fs.readText` * fix: use `FileTypes.TEXT` * fix: trim prefix "file://" with `replace` * refactor(FileAction): centralize file click handling for text preview - Use FileAction.handleClick in AttachmentPreview and MessageAttachments - Show i18n error modal on failure (zh-cn: files.click.error) * fix: i18n * fix: update i18n on field `files.click.error` with codex * fix: use hook * fix: rename `handleClick` to `preview` * feat: support lang highlight * fix: remove prefix '.' of extension * fix: code editor style * fix: editor cursor text style * fix: add `FileTypes` check * fix: move parseFileType into utils * fix: move `parseFileTypes` into utils/file
This commit is contained in:
parent
9b1aa3cd36
commit
0c589a6f79
105
src/renderer/src/components/Popups/TextFilePreview.tsx
Normal file
105
src/renderer/src/components/Popups/TextFilePreview.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import CodeEditor from '../CodeEditor'
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
title: string
|
||||
extension?: string
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ text, title, extension, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
TextFilePreviewPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
title={title}
|
||||
width={700}
|
||||
transitionName="animation-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
borderRadius: 20,
|
||||
padding: 0,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
body: {
|
||||
height: '80vh',
|
||||
maxHeight: 'inherit',
|
||||
padding: 0
|
||||
}
|
||||
}}
|
||||
centered
|
||||
closable={true}
|
||||
footer={null}>
|
||||
{extension !== undefined ? (
|
||||
<Editor
|
||||
editable={false}
|
||||
expanded={false}
|
||||
height="100%"
|
||||
style={{ height: '100%' }}
|
||||
value={text}
|
||||
language={extension}
|
||||
/>
|
||||
) : (
|
||||
<Text>{text}</Text>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const Text = styled.div`
|
||||
padding: 16px;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
`
|
||||
|
||||
const Editor = styled(CodeEditor)`
|
||||
.cm-line {
|
||||
cursor: text;
|
||||
}
|
||||
`
|
||||
|
||||
export default class TextFilePreviewPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('TextFilePreviewPopup')
|
||||
}
|
||||
static show(text: string, title: string, extension?: string) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
text={text}
|
||||
title={title}
|
||||
extension={extension}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide('TextFilePreviewPopup')
|
||||
}}
|
||||
/>,
|
||||
'TextFilePreviewPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
35
src/renderer/src/hooks/useAttachment.ts
Normal file
35
src/renderer/src/hooks/useAttachment.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { loggerService } from '@logger'
|
||||
import TextFilePreviewPopup from '@renderer/components/Popups/TextFilePreview'
|
||||
import { FileTypes } from '@renderer/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const logger = loggerService.withContext('FileAction')
|
||||
|
||||
/**
|
||||
* 处理附件点击事件:
|
||||
* 如果是文本文件,在 Preview 视图中打开,
|
||||
* 否则使用默认打开接口
|
||||
*/
|
||||
export function useAttachment() {
|
||||
const { t } = useTranslation()
|
||||
const preview = async (path: string, title: string, fileType: FileTypes, extension?: string) => {
|
||||
try {
|
||||
if (fileType === FileTypes.TEXT) {
|
||||
const content = await window.api.fs.readText(path)
|
||||
let ext = extension
|
||||
if (ext?.startsWith('.')) {
|
||||
ext = ext.replace('.', '')
|
||||
}
|
||||
TextFilePreviewPopup.show(content, title, ext)
|
||||
} else {
|
||||
window.api.file.openPath(path)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error opening ${path}:`, err as Error)
|
||||
window.modal.error({ content: t('files.preview.error'), centered: true })
|
||||
}
|
||||
}
|
||||
return {
|
||||
preview
|
||||
}
|
||||
}
|
||||
@ -947,6 +947,9 @@
|
||||
"image": "Image",
|
||||
"name": "Name",
|
||||
"open": "Open",
|
||||
"preview": {
|
||||
"error": "Failed to open file"
|
||||
},
|
||||
"size": "Size",
|
||||
"text": "Text",
|
||||
"title": "Files",
|
||||
|
||||
@ -947,6 +947,9 @@
|
||||
"image": "图片",
|
||||
"name": "文件名",
|
||||
"open": "打开",
|
||||
"preview": {
|
||||
"error": "打开文件失败"
|
||||
},
|
||||
"size": "大小",
|
||||
"text": "文本",
|
||||
"title": "文件",
|
||||
|
||||
@ -947,6 +947,9 @@
|
||||
"image": "圖片",
|
||||
"name": "名稱",
|
||||
"open": "開啟",
|
||||
"preview": {
|
||||
"error": "開啟檔案失敗"
|
||||
},
|
||||
"size": "大小",
|
||||
"text": "文字",
|
||||
"title": "檔案",
|
||||
|
||||
@ -943,6 +943,9 @@
|
||||
"image": "Εικόνα",
|
||||
"name": "Όνομα αρχείου",
|
||||
"open": "Άνοιγμα",
|
||||
"preview": {
|
||||
"error": "Αποτυχία ανοίγματος του αρχείου"
|
||||
},
|
||||
"size": "Μέγεθος",
|
||||
"text": "Κείμενο",
|
||||
"title": "Αρχεία",
|
||||
|
||||
@ -943,6 +943,9 @@
|
||||
"image": "Imagen",
|
||||
"name": "Nombre del archivo",
|
||||
"open": "Abrir",
|
||||
"preview": {
|
||||
"error": "No se pudo abrir el archivo"
|
||||
},
|
||||
"size": "Tamaño",
|
||||
"text": "Texto",
|
||||
"title": "Archivo",
|
||||
|
||||
@ -943,6 +943,9 @@
|
||||
"image": "Image",
|
||||
"name": "Nom du fichier",
|
||||
"open": "Ouvrir",
|
||||
"preview": {
|
||||
"error": "Échec de l’ouverture du fichier"
|
||||
},
|
||||
"size": "Taille",
|
||||
"text": "Texte",
|
||||
"title": "Fichier",
|
||||
|
||||
@ -943,6 +943,9 @@
|
||||
"image": "画像",
|
||||
"name": "名前",
|
||||
"open": "開く",
|
||||
"preview": {
|
||||
"error": "ファイルを開くのに失敗しました"
|
||||
},
|
||||
"size": "サイズ",
|
||||
"text": "テキスト",
|
||||
"title": "ファイル",
|
||||
|
||||
@ -943,6 +943,9 @@
|
||||
"image": "Imagem",
|
||||
"name": "Nome do Arquivo",
|
||||
"open": "Abrir",
|
||||
"preview": {
|
||||
"error": "Falha ao abrir o arquivo"
|
||||
},
|
||||
"size": "Tamanho",
|
||||
"text": "Texto",
|
||||
"title": "Arquivo",
|
||||
|
||||
@ -943,6 +943,9 @@
|
||||
"image": "Изображение",
|
||||
"name": "Имя",
|
||||
"open": "Открыть",
|
||||
"preview": {
|
||||
"error": "Не удалось открыть файл"
|
||||
},
|
||||
"size": "Размер",
|
||||
"text": "Текст",
|
||||
"title": "Файлы",
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
LinkOutlined
|
||||
} from '@ant-design/icons'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { useAttachment } from '@renderer/hooks/useAttachment'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { FileMetadata } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
@ -81,6 +82,7 @@ export const getFileIcon = (type?: string) => {
|
||||
}
|
||||
|
||||
export const FileNameRender: FC<{ file: FileMetadata }> = ({ file }) => {
|
||||
const { preview } = useAttachment()
|
||||
const [visible, setVisible] = useState<boolean>(false)
|
||||
const isImage = (ext: string) => {
|
||||
return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'].includes(ext.toLocaleLowerCase())
|
||||
@ -121,9 +123,8 @@ export const FileNameRender: FC<{ file: FileMetadata }> = ({ file }) => {
|
||||
return
|
||||
}
|
||||
const path = FileManager.getSafePath(file)
|
||||
if (path) {
|
||||
window.api.file.openPath(path)
|
||||
}
|
||||
const name = FileManager.formatFileName(file)
|
||||
preview(path, name, file.type, file.ext)
|
||||
}}
|
||||
title={fullName}>
|
||||
{displayName}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { useAttachment } from '@renderer/hooks/useAttachment'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import type { FileMessageBlock } from '@renderer/types/newMessage'
|
||||
import { parseFileTypes } from '@renderer/utils'
|
||||
import { Upload } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -20,6 +23,7 @@ const StyledUpload = styled(Upload)`
|
||||
`
|
||||
|
||||
const MessageAttachments: FC<Props> = ({ block }) => {
|
||||
const { preview } = useAttachment()
|
||||
if (!block.file) {
|
||||
return null
|
||||
}
|
||||
@ -34,9 +38,26 @@ const MessageAttachments: FC<Props> = ({ block }) => {
|
||||
uid: block.file.id,
|
||||
url: 'file://' + FileManager.getSafePath(block.file),
|
||||
status: 'done' as const,
|
||||
name: FileManager.formatFileName(block.file)
|
||||
name: FileManager.formatFileName(block.file),
|
||||
type: block.file.type,
|
||||
preview: block.file.ext
|
||||
}
|
||||
]}
|
||||
onPreview={(file) => {
|
||||
if (file.url === undefined || file.type === undefined) {
|
||||
return
|
||||
}
|
||||
const fileType = parseFileTypes(file.type)
|
||||
if (fileType === null) {
|
||||
window.modal.error({ content: t('files.preview.error'), centered: true })
|
||||
return
|
||||
}
|
||||
let path = file.url
|
||||
if (path.startsWith('file://')) {
|
||||
path = path.replace('file://', '')
|
||||
}
|
||||
preview(path, file.name, fileType, file.preview)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@ -129,3 +129,10 @@ export const mime2type = (mimeStr: string): FileTypes => {
|
||||
}
|
||||
return FileTypes.OTHER
|
||||
}
|
||||
|
||||
export function parseFileTypes(str: string): FileTypes | null {
|
||||
if (Object.values(FileTypes).includes(str as FileTypes)) {
|
||||
return str as FileTypes
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user