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:
自由的世界人 2025-06-24 18:51:58 +08:00 committed by GitHub
parent f2c9bf433e
commit e2b8133729
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 143 additions and 134 deletions

View File

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

View File

@ -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);
}
}
`

View 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 })
}
}