refactor: update file management to use filetype instead of filemetadata

This commit is contained in:
kangfenmao 2024-09-14 16:08:43 +08:00
parent 9ae9fdf392
commit aa427c9911
12 changed files with 45 additions and 52 deletions

View File

@ -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 }

View File

@ -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,

View File

@ -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
} }

View File

@ -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: {

View File

@ -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({

View File

@ -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(() => {

View File

@ -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>

View File

@ -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
} }

View File

@ -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 }) => {

View File

@ -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>

View File

@ -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()
} }
} }

View File

@ -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',