mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 12:51:26 +08:00
refactor: file actions into FileAction service (#7413)
* refactor: file actions into FileAction service Moved file sorting, deletion, and renaming logic from FilesPage to a new FileAction service for better modularity and reuse. Updated FileList and FilesPage to use the new service functions, and improved the delete button UI in FileList.
This commit is contained in:
parent
f2c9bf433e
commit
e2b8133729
@ -1,3 +1,5 @@
|
||||
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { handleDelete } from '@renderer/services/FileAction'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { FileType, FileTypes } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
@ -48,6 +50,24 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
||||
<ImageInfo>
|
||||
<div>{formatFileSize(file.size)}</div>
|
||||
</ImageInfo>
|
||||
<DeleteButton
|
||||
title={t('files.delete.title')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
window.modal.confirm({
|
||||
title: t('files.delete.title'),
|
||||
content: t('files.delete.content'),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
centered: true,
|
||||
onOk: () => {
|
||||
handleDelete(file.id, t)
|
||||
},
|
||||
icon: <ExclamationCircleOutlined style={{ color: 'red' }} />
|
||||
})
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
</DeleteButton>
|
||||
</ImageWrapper>
|
||||
</Col>
|
||||
))}
|
||||
@ -159,4 +179,26 @@ const ImageInfo = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const DeleteButton = styled.div`
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 0, 0, 0.8);
|
||||
}
|
||||
`
|
||||
|
||||
export default memo(FileList)
|
||||
|
||||
@ -7,13 +7,10 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Logger from '@renderer/config/logger'
|
||||
import db from '@renderer/databases'
|
||||
import { handleDelete, handleRename, sortFiles, tempFilesSort } from '@renderer/services/FileAction'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import store from '@renderer/store'
|
||||
import { FileType, FileTypes } from '@renderer/types'
|
||||
import { Message } from '@renderer/types/newMessage'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Button, Empty, Flex, Popconfirm } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
@ -34,34 +31,6 @@ const FilesPage: FC = () => {
|
||||
const [sortField, setSortField] = useState<SortField>('created_at')
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
|
||||
|
||||
const tempFilesSort = (files: FileType[]) => {
|
||||
return files.sort((a, b) => {
|
||||
const aIsTemp = a.origin_name.startsWith('temp_file')
|
||||
const bIsTemp = b.origin_name.startsWith('temp_file')
|
||||
if (aIsTemp && !bIsTemp) return 1
|
||||
if (!aIsTemp && bIsTemp) return -1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
const sortFiles = (files: FileType[]) => {
|
||||
return [...files].sort((a, b) => {
|
||||
let comparison = 0
|
||||
switch (sortField) {
|
||||
case 'created_at':
|
||||
comparison = dayjs(a.created_at).unix() - dayjs(b.created_at).unix()
|
||||
break
|
||||
case 'size':
|
||||
comparison = a.size - b.size
|
||||
break
|
||||
case 'name':
|
||||
comparison = a.origin_name.localeCompare(b.origin_name)
|
||||
break
|
||||
}
|
||||
return sortOrder === 'asc' ? comparison : -comparison
|
||||
})
|
||||
}
|
||||
|
||||
const files = useLiveQuery<FileType[]>(() => {
|
||||
if (fileType === 'all') {
|
||||
return db.files.orderBy('count').toArray().then(tempFilesSort)
|
||||
@ -69,106 +38,7 @@ const FilesPage: FC = () => {
|
||||
return db.files.where('type').equals(fileType).sortBy('count').then(tempFilesSort)
|
||||
}, [fileType])
|
||||
|
||||
const sortedFiles = files ? sortFiles(files) : []
|
||||
|
||||
const handleDelete = async (fileId: string) => {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
if (!file) return
|
||||
|
||||
const paintings = await store.getState().paintings.paintings
|
||||
const paintingsFiles = paintings.flatMap((p) => p.files)
|
||||
|
||||
if (paintingsFiles.some((p) => p.id === fileId)) {
|
||||
window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true })
|
||||
return
|
||||
}
|
||||
if (file) {
|
||||
await FileManager.deleteFile(fileId, true)
|
||||
}
|
||||
|
||||
const relatedBlocks = await db.message_blocks.where('file.id').equals(fileId).toArray()
|
||||
|
||||
const blockIdsToDelete = relatedBlocks.map((block) => block.id)
|
||||
|
||||
const blocksByMessageId: Record<string, string[]> = {}
|
||||
for (const block of relatedBlocks) {
|
||||
if (!blocksByMessageId[block.messageId]) {
|
||||
blocksByMessageId[block.messageId] = []
|
||||
}
|
||||
blocksByMessageId[block.messageId].push(block.id)
|
||||
}
|
||||
|
||||
try {
|
||||
const affectedMessageIds = [...new Set(relatedBlocks.map((b) => b.messageId))]
|
||||
|
||||
if (affectedMessageIds.length === 0 && blockIdsToDelete.length > 0) {
|
||||
// This case should ideally not happen if relatedBlocks were found,
|
||||
// but handle it just in case: only delete blocks.
|
||||
await db.message_blocks.bulkDelete(blockIdsToDelete)
|
||||
Logger.log(
|
||||
`Deleted ${blockIdsToDelete.length} blocks related to file ${fileId}. No associated messages found (unexpected).`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await db.transaction('rw', db.topics, db.message_blocks, async () => {
|
||||
// Fetch all topics (potential performance bottleneck if many topics)
|
||||
const allTopics = await db.topics.toArray()
|
||||
const topicsToUpdate: Record<string, { messages: Message[] }> = {} // Store updates keyed by topicId
|
||||
|
||||
for (const topic of allTopics) {
|
||||
let topicModified = false
|
||||
// Ensure topic.messages exists and is an array before mapping
|
||||
const currentMessages = Array.isArray(topic.messages) ? topic.messages : []
|
||||
const updatedMessages = currentMessages.map((message) => {
|
||||
// Check if this message is affected
|
||||
if (affectedMessageIds.includes(message.id)) {
|
||||
// Ensure message.blocks exists and is an array
|
||||
const currentBlocks = Array.isArray(message.blocks) ? message.blocks : []
|
||||
const originalBlockCount = currentBlocks.length
|
||||
// Filter out the blocks marked for deletion
|
||||
const newBlocks = currentBlocks.filter((blockId) => !blockIdsToDelete.includes(blockId))
|
||||
if (newBlocks.length < originalBlockCount) {
|
||||
topicModified = true
|
||||
return { ...message, blocks: newBlocks } // Return updated message
|
||||
}
|
||||
}
|
||||
return message // Return original message
|
||||
})
|
||||
|
||||
if (topicModified) {
|
||||
// Store the update for this topic
|
||||
topicsToUpdate[topic.id] = { messages: updatedMessages }
|
||||
}
|
||||
}
|
||||
|
||||
// Apply updates to topics
|
||||
const updatePromises = Object.entries(topicsToUpdate).map(([topicId, updateData]) =>
|
||||
db.topics.update(topicId, updateData)
|
||||
)
|
||||
await Promise.all(updatePromises)
|
||||
|
||||
// Finally, delete the MessageBlocks
|
||||
await db.message_blocks.bulkDelete(blockIdsToDelete)
|
||||
})
|
||||
|
||||
Logger.log(`Deleted ${blockIdsToDelete.length} blocks and updated relevant topic messages for file ${fileId}.`)
|
||||
} catch (error) {
|
||||
Logger.error(`Error updating topics or deleting blocks for file ${fileId}:`, error)
|
||||
window.modal.error({ content: t('files.delete.db_error'), centered: true }) // 提示数据库操作失败
|
||||
// Consider whether to attempt to restore the physical file (usually difficult)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRename = async (fileId: string) => {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
if (file) {
|
||||
const newName = await TextEditPopup.show({ text: file.origin_name })
|
||||
if (newName) {
|
||||
FileManager.updateFile({ ...file, origin_name: newName })
|
||||
}
|
||||
}
|
||||
}
|
||||
const sortedFiles = files ? sortFiles(files, sortField, sortOrder) : []
|
||||
|
||||
const dataSource = sortedFiles?.map((file) => {
|
||||
return {
|
||||
@ -189,7 +59,7 @@ const FilesPage: FC = () => {
|
||||
description={t('files.delete.content')}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={() => handleDelete(file.id)}
|
||||
onConfirm={() => handleDelete(file.id, t)}
|
||||
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
|
||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
@ -310,7 +180,6 @@ const SideNav = styled.div`
|
||||
background-color: var(--color-background-soft);
|
||||
color: var(--color-primary);
|
||||
border: 0.5px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
98
src/renderer/src/services/FileAction.ts
Normal file
98
src/renderer/src/services/FileAction.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Logger from '@renderer/config/logger'
|
||||
import db from '@renderer/databases'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import store from '@renderer/store'
|
||||
import { FileType } from '@renderer/types'
|
||||
import { Message } from '@renderer/types/newMessage'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 排序相关
|
||||
export type SortField = 'created_at' | 'size' | 'name'
|
||||
export type SortOrder = 'asc' | 'desc'
|
||||
|
||||
export function tempFilesSort(files: FileType[]): FileType[] {
|
||||
return files.sort((a, b) => {
|
||||
const aIsTemp = a.origin_name.startsWith('temp_file')
|
||||
const bIsTemp = b.origin_name.startsWith('temp_file')
|
||||
if (aIsTemp && !bIsTemp) return 1
|
||||
if (!aIsTemp && bIsTemp) return -1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
export function sortFiles(files: FileType[], sortField: SortField, sortOrder: SortOrder): FileType[] {
|
||||
return [...files].sort((a, b) => {
|
||||
let comparison = 0
|
||||
switch (sortField) {
|
||||
case 'created_at':
|
||||
comparison = dayjs(a.created_at).unix() - dayjs(b.created_at).unix()
|
||||
break
|
||||
case 'size':
|
||||
comparison = a.size - b.size
|
||||
break
|
||||
case 'name':
|
||||
comparison = a.origin_name.localeCompare(b.origin_name)
|
||||
break
|
||||
}
|
||||
return sortOrder === 'asc' ? comparison : -comparison
|
||||
})
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
export async function handleDelete(fileId: string, t: (key: string) => string) {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
if (!file) return
|
||||
|
||||
const paintings = await store.getState().paintings.paintings
|
||||
const paintingsFiles = paintings.flatMap((p) => p.files)
|
||||
|
||||
if (paintingsFiles.some((p) => p.id === fileId)) {
|
||||
window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true })
|
||||
return
|
||||
}
|
||||
await FileManager.deleteFile(fileId, true)
|
||||
|
||||
const relatedBlocks = await db.message_blocks.where('file.id').equals(fileId).toArray()
|
||||
const blockIdsToDelete = relatedBlocks.map((b) => b.id)
|
||||
const affectedMessageIds = [...new Set(relatedBlocks.map((b) => b.messageId))]
|
||||
|
||||
try {
|
||||
await db.transaction('rw', db.topics, db.message_blocks, async () => {
|
||||
const allTopics = await db.topics.toArray()
|
||||
const topicsToUpdate: Record<string, { messages: Message[] }> = {}
|
||||
|
||||
for (const topic of allTopics) {
|
||||
let modified = false
|
||||
const newMessages = (topic.messages || []).map((msg) => {
|
||||
if (affectedMessageIds.includes(msg.id)) {
|
||||
const filtered = (msg.blocks || []).filter((blk) => !blockIdsToDelete.includes(blk))
|
||||
if (filtered.length < (msg.blocks || []).length) {
|
||||
modified = true
|
||||
return { ...msg, blocks: filtered }
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
if (modified) topicsToUpdate[topic.id] = { messages: newMessages }
|
||||
}
|
||||
|
||||
await Promise.all(Object.entries(topicsToUpdate).map(([id, data]) => db.topics.update(id, data)))
|
||||
await db.message_blocks.bulkDelete(blockIdsToDelete)
|
||||
})
|
||||
Logger.log(`Deleted ${blockIdsToDelete.length} blocks for file ${fileId}`)
|
||||
} catch (err) {
|
||||
Logger.error(`Error removing file blocks for ${fileId}:`, err)
|
||||
window.modal.error({ content: t('files.delete.db_error'), centered: true })
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名操作
|
||||
export async function handleRename(fileId: string) {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
if (!file) return
|
||||
const newName = await TextEditPopup.show({ text: file.origin_name })
|
||||
if (newName) {
|
||||
FileManager.updateFile({ ...file, origin_name: newName })
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user