mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 14:29:15 +08:00
refactor: update file management to use filetype instead of filemetadata
This commit is contained in:
parent
9ae9fdf392
commit
aa427c9911
@ -1,4 +1,4 @@
|
|||||||
import { FileMetadata } from '@types'
|
import { FileType } from '@types'
|
||||||
import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron'
|
import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
@ -55,7 +55,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
|
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
|
||||||
ipcMain.handle('file:upload', async (_, file: FileMetadata) => await fileManager.uploadFile(file))
|
ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file))
|
||||||
ipcMain.handle('file:delete', async (_, fileId: string) => {
|
ipcMain.handle('file:delete', async (_, fileId: string) => {
|
||||||
await fileManager.deleteFile(fileId)
|
await fileManager.deleteFile(fileId)
|
||||||
return { success: true }
|
return { success: true }
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { getFileType } from '@main/utils/file'
|
import { getFileType } from '@main/utils/file'
|
||||||
import { FileMetadata } from '@types'
|
import { FileType } from '@types'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import { app, dialog, OpenDialogOptions } from 'electron'
|
import { app, dialog, OpenDialogOptions } from 'electron'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
@ -30,7 +30,7 @@ class File {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async findDuplicateFile(filePath: string): Promise<FileMetadata | null> {
|
async findDuplicateFile(filePath: string): Promise<FileType | null> {
|
||||||
const stats = fs.statSync(filePath)
|
const stats = fs.statSync(filePath)
|
||||||
const fileSize = stats.size
|
const fileSize = stats.size
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ class File {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectFile(options?: OpenDialogOptions): Promise<FileMetadata[] | null> {
|
async selectFile(options?: OpenDialogOptions): Promise<FileType[] | null> {
|
||||||
const defaultOptions: OpenDialogOptions = {
|
const defaultOptions: OpenDialogOptions = {
|
||||||
properties: ['openFile']
|
properties: ['openFile']
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ class File {
|
|||||||
return Promise.all(fileMetadataPromises)
|
return Promise.all(fileMetadataPromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(file: FileMetadata): Promise<FileMetadata> {
|
async uploadFile(file: FileType): Promise<FileType> {
|
||||||
const duplicateFile = await this.findDuplicateFile(file.path)
|
const duplicateFile = await this.findDuplicateFile(file.path)
|
||||||
|
|
||||||
if (duplicateFile) {
|
if (duplicateFile) {
|
||||||
@ -116,7 +116,7 @@ class File {
|
|||||||
const stats = await fs.promises.stat(destPath)
|
const stats = await fs.promises.stat(destPath)
|
||||||
const fileType = getFileType(ext)
|
const fileType = getFileType(ext)
|
||||||
|
|
||||||
const fileMetadata: FileMetadata = {
|
const fileMetadata: FileType = {
|
||||||
id: uuid,
|
id: uuid,
|
||||||
origin_name,
|
origin_name,
|
||||||
name: uuid + ext,
|
name: uuid + ext,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import logger from 'electron-log'
|
|||||||
import { writeFile } from 'fs'
|
import { writeFile } from 'fs'
|
||||||
import { readFile } from 'fs/promises'
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
import { FileType } from '../../renderer/src/types'
|
import { FileTypes } from '../../renderer/src/types'
|
||||||
|
|
||||||
export async function saveFile(
|
export async function saveFile(
|
||||||
_: Electron.IpcMainInvokeEvent,
|
_: Electron.IpcMainInvokeEvent,
|
||||||
@ -56,16 +56,16 @@ export async function openFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFileType(ext: string): FileType {
|
export function getFileType(ext: string): FileTypes {
|
||||||
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||||
const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||||
const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||||
const documentExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt']
|
const documentExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt']
|
||||||
|
|
||||||
ext = ext.toLowerCase()
|
ext = ext.toLowerCase()
|
||||||
if (imageExts.includes(ext)) return FileType.IMAGE
|
if (imageExts.includes(ext)) return FileTypes.IMAGE
|
||||||
if (videoExts.includes(ext)) return FileType.VIDEO
|
if (videoExts.includes(ext)) return FileTypes.VIDEO
|
||||||
if (audioExts.includes(ext)) return FileType.AUDIO
|
if (audioExts.includes(ext)) return FileTypes.AUDIO
|
||||||
if (documentExts.includes(ext)) return FileType.DOCUMENT
|
if (documentExts.includes(ext)) return FileTypes.DOCUMENT
|
||||||
return FileType.OTHER
|
return FileTypes.OTHER
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/preload/index.d.ts
vendored
6
src/preload/index.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
import { FileMetadata } from '@renderer/types'
|
import { FileType } from '@renderer/types'
|
||||||
import type { OpenDialogOptions } from 'electron'
|
import type { OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -22,8 +22,8 @@ declare global {
|
|||||||
compress: (text: string) => Promise<Buffer>
|
compress: (text: string) => Promise<Buffer>
|
||||||
decompress: (text: Buffer) => Promise<string>
|
decompress: (text: Buffer) => Promise<string>
|
||||||
file: {
|
file: {
|
||||||
select: (options?: OpenDialogOptions) => Promise<FileMetadata[] | null>
|
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||||
upload: (file: FileMetadata) => Promise<FileMetadata>
|
upload: (file: FileType) => Promise<FileType>
|
||||||
delete: (fileId: string) => Promise<{ success: boolean }>
|
delete: (fileId: string) => Promise<{ success: boolean }>
|
||||||
}
|
}
|
||||||
image: {
|
image: {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { FileMetadata } from '@renderer/types'
|
import { FileType } from '@renderer/types'
|
||||||
import { Dexie, type EntityTable } from 'dexie'
|
import { Dexie, type EntityTable } from 'dexie'
|
||||||
|
|
||||||
// Database declaration (move this to its own module also)
|
// Database declaration (move this to its own module also)
|
||||||
export const db = new Dexie('CherryStudio') as Dexie & {
|
export const db = new Dexie('CherryStudio') as Dexie & {
|
||||||
files: EntityTable<FileMetadata, 'id'>
|
files: EntityTable<FileType, 'id'>
|
||||||
}
|
}
|
||||||
|
|
||||||
db.version(1).stores({
|
db.version(1).stores({
|
||||||
|
|||||||
@ -11,8 +11,7 @@ import { useSettings } from './useSettings'
|
|||||||
|
|
||||||
export function useAppInit() {
|
export function useAppInit() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { proxyUrl } = useSettings()
|
const { proxyUrl, language } = useSettings()
|
||||||
const { language } = useSettings()
|
|
||||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import { VStack } from '@renderer/components/Layout'
|
import { VStack } from '@renderer/components/Layout'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import FileManager from '@renderer/services/file'
|
import { FileType } from '@renderer/types'
|
||||||
import { FileMetadata } from '@renderer/types'
|
import { Image, Table } from 'antd'
|
||||||
import { Button, Image, Table } from 'antd'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@ -12,9 +11,10 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
const FilesPage: FC = () => {
|
const FilesPage: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const files = useLiveQuery<FileMetadata[]>(() => db.files.toArray())
|
const files = useLiveQuery<FileType[]>(() => db.files.toArray())
|
||||||
|
|
||||||
const dataSource = files?.map((file) => ({
|
const dataSource = files?.map((file) => ({
|
||||||
|
key: file.id,
|
||||||
file: <Image src={'file://' + file.path} preview={false} style={{ maxHeight: '40px' }} />,
|
file: <Image src={'file://' + file.path} preview={false} style={{ maxHeight: '40px' }} />,
|
||||||
name: <a href={'file://' + file.path}>{file.name}</a>,
|
name: <a href={'file://' + file.path}>{file.name}</a>,
|
||||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||||
@ -25,7 +25,8 @@ const FilesPage: FC = () => {
|
|||||||
{
|
{
|
||||||
title: t('files.file'),
|
title: t('files.file'),
|
||||||
dataIndex: 'file',
|
dataIndex: 'file',
|
||||||
key: 'file'
|
key: 'file',
|
||||||
|
width: '300px'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('files.name'),
|
title: t('files.name'),
|
||||||
@ -52,13 +53,6 @@ const FilesPage: FC = () => {
|
|||||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
const files = await FileManager.selectFiles()
|
|
||||||
files && FileManager.uploadFiles(files)
|
|
||||||
}}>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
<VStack style={{ flex: 1 }}>
|
<VStack style={{ flex: 1 }}>
|
||||||
<Table dataSource={dataSource} columns={columns} style={{ width: '100%', height: '100%' }} size="small" />
|
<Table dataSource={dataSource} columns={columns} style={{ width: '100%', height: '100%' }} size="small" />
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { PaperClipOutlined } from '@ant-design/icons'
|
import { PaperClipOutlined } from '@ant-design/icons'
|
||||||
import { isVisionModel } from '@renderer/config/models'
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
import { FileMetadata, Model } from '@renderer/types'
|
import { FileType, Model } from '@renderer/types'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
model: Model
|
model: Model
|
||||||
files: FileMetadata[]
|
files: FileType[]
|
||||||
setFiles: (files: FileMetadata[]) => void
|
setFiles: (files: FileType[]) => void
|
||||||
ToolbarButton: any
|
ToolbarButton: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { FileMetadata } from '@renderer/types'
|
import { FileType } from '@renderer/types'
|
||||||
import { Upload } from 'antd'
|
import { Upload } from 'antd'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
files: FileMetadata[]
|
files: FileType[]
|
||||||
setFiles: (files: FileMetadata[]) => void
|
setFiles: (files: FileType[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
|
const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
ClearOutlined,
|
ClearOutlined,
|
||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
|
FormOutlined,
|
||||||
FullscreenExitOutlined,
|
FullscreenExitOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
PlusCircleOutlined,
|
|
||||||
QuestionCircleOutlined
|
QuestionCircleOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
@ -17,7 +17,7 @@ import FileManager from '@renderer/services/file'
|
|||||||
import { estimateInputTokenCount } from '@renderer/services/messages'
|
import { estimateInputTokenCount } from '@renderer/services/messages'
|
||||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||||
import { Assistant, FileMetadata, Message, Topic } from '@renderer/types'
|
import { Assistant, FileType, Message, Topic } from '@renderer/types'
|
||||||
import { delay, uuid } from '@renderer/utils'
|
import { delay, uuid } from '@renderer/utils'
|
||||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
@ -49,7 +49,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const [contextCount, setContextCount] = useState(0)
|
const [contextCount, setContextCount] = useState(0)
|
||||||
const generating = useAppSelector((state) => state.runtime.generating)
|
const generating = useAppSelector((state) => state.runtime.generating)
|
||||||
const textareaRef = useRef<TextAreaRef>(null)
|
const textareaRef = useRef<TextAreaRef>(null)
|
||||||
const [files, setFiles] = useState<FileMetadata[]>([])
|
const [files, setFiles] = useState<FileType[]>([])
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const containerRef = useRef(null)
|
const containerRef = useRef(null)
|
||||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||||
@ -229,7 +229,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
<ToolbarMenu>
|
<ToolbarMenu>
|
||||||
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
||||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||||
<PlusCircleOutlined />
|
<FormOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { FileMetadata } from '@renderer/types'
|
import { FileType } from '@renderer/types'
|
||||||
|
|
||||||
class FileManager {
|
class FileManager {
|
||||||
static async selectFiles(options?: Electron.OpenDialogOptions): Promise<FileMetadata[] | null> {
|
static async selectFiles(options?: Electron.OpenDialogOptions): Promise<FileType[] | null> {
|
||||||
const files = await window.api.file.select(options)
|
const files = await window.api.file.select(options)
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
static async uploadFile(file: FileMetadata): Promise<FileMetadata> {
|
static async uploadFile(file: FileType): Promise<FileType> {
|
||||||
const uploadFile = await window.api.file.upload(file)
|
const uploadFile = await window.api.file.upload(file)
|
||||||
const fileRecord = await db.files.get(uploadFile.id)
|
const fileRecord = await db.files.get(uploadFile.id)
|
||||||
|
|
||||||
@ -21,11 +21,11 @@ class FileManager {
|
|||||||
return uploadFile
|
return uploadFile
|
||||||
}
|
}
|
||||||
|
|
||||||
static async uploadFiles(files: FileMetadata[]): Promise<FileMetadata[]> {
|
static async uploadFiles(files: FileType[]): Promise<FileType[]> {
|
||||||
return Promise.all(files.map((file) => this.uploadFile(file)))
|
return Promise.all(files.map((file) => this.uploadFile(file)))
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getFile(id: string): Promise<FileMetadata | undefined> {
|
static async getFile(id: string): Promise<FileType | undefined> {
|
||||||
return db.files.get(id)
|
return db.files.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ class FileManager {
|
|||||||
await Promise.all(ids.map((id) => this.deleteFile(id)))
|
await Promise.all(ids.map((id) => this.deleteFile(id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
static async allFiles(): Promise<FileMetadata[]> {
|
static async allFiles(): Promise<FileType[]> {
|
||||||
return db.files.toArray()
|
return db.files.toArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export type Message = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
status: 'sending' | 'pending' | 'success' | 'paused' | 'error'
|
status: 'sending' | 'pending' | 'success' | 'paused' | 'error'
|
||||||
modelId?: string
|
modelId?: string
|
||||||
files?: FileMetadata[]
|
files?: FileType[]
|
||||||
images?: string[]
|
images?: string[]
|
||||||
usage?: OpenAI.Completions.CompletionUsage
|
usage?: OpenAI.Completions.CompletionUsage
|
||||||
type?: 'text' | '@' | 'clear'
|
type?: 'text' | '@' | 'clear'
|
||||||
@ -87,7 +87,7 @@ export type MinAppType = {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileMetadata {
|
export interface FileType {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
origin_name: string
|
origin_name: string
|
||||||
@ -99,7 +99,7 @@ export interface FileMetadata {
|
|||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FileType {
|
export enum FileTypes {
|
||||||
IMAGE = 'image',
|
IMAGE = 'image',
|
||||||
VIDEO = 'video',
|
VIDEO = 'video',
|
||||||
AUDIO = 'audio',
|
AUDIO = 'audio',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user