cc-haha/desktop/src/lib/composerAttachments.ts
2026-07-13 10:22:41 +08:00

114 lines
3.6 KiB
TypeScript

import { isDesktopRuntime } from './desktopRuntime'
import { getDesktopHost } from './desktopHost'
export type ComposerAttachment = {
id: string
name: string
type: 'image' | 'file'
path?: string
mimeType?: string
previewUrl?: string
data?: string
isDirectory?: boolean
lineStart?: number
lineEnd?: number
diffSide?: 'old' | 'new'
hunkId?: string
note?: string
quote?: string
}
function nextAttachmentId() {
return `att-${Date.now()}-${Math.random().toString(36).slice(2)}`
}
export function getFileNameFromPath(filePath: string): string {
const normalized = filePath.replace(/[\\/]+$/g, '')
return normalized.split(/[\\/]/).filter(Boolean).pop() || filePath
}
export function pathToComposerAttachment(filePath: string): ComposerAttachment {
return {
id: nextAttachmentId(),
name: getFileNameFromPath(filePath),
type: 'file',
path: filePath,
}
}
export function pathsToComposerAttachments(filePaths: string[]): ComposerAttachment[] {
return filePaths
.filter((filePath) => typeof filePath === 'string' && filePath.length > 0)
.map(pathToComposerAttachment)
}
export function dataTransferHasFiles(dataTransfer: DataTransfer): boolean {
const types = Array.from(dataTransfer.types ?? [])
return types.includes('Files') || dataTransfer.files.length > 0
}
export async function dataTransferToComposerAttachments(dataTransfer: DataTransfer): Promise<ComposerAttachment[]> {
return filesToComposerAttachments(dataTransfer.files)
}
export async function selectNativeFileAttachments(): Promise<ComposerAttachment[] | null> {
const host = getDesktopHost()
if (!host.isDesktop || !host.capabilities.dialogs) return null
try {
const selected = await host.dialogs.open({
multiple: true,
directory: false,
})
const paths = normalizeDialogSelection(selected)
return pathsToComposerAttachments(paths)
} catch (error) {
console.warn('[attachments] Native file picker failed; falling back to browser file input', error)
return null
}
}
export async function filesToComposerAttachments(files: FileList | File[]): Promise<ComposerAttachment[]> {
const entries = Array.from(files)
const attachments = await Promise.all(entries.map(fileToComposerAttachment))
return attachments.filter((attachment): attachment is ComposerAttachment => !!attachment)
}
function normalizeDialogSelection(selected: string | string[] | null): string[] {
if (!selected) return []
const paths = Array.isArray(selected) ? selected : [selected]
return paths.filter((filePath) => typeof filePath === 'string' && filePath.length > 0)
}
function getNativeFilePath(file: File): string | undefined {
const path = (file as File & { path?: unknown }).path
return typeof path === 'string' && path.length > 0 ? path : undefined
}
async function fileToComposerAttachment(file: File): Promise<ComposerAttachment | null> {
const nativePath = isDesktopRuntime() ? getNativeFilePath(file) : undefined
if (nativePath) {
return pathToComposerAttachment(nativePath)
}
const isImage = file.type.startsWith('image/')
const data = await readFileAsDataUrl(file)
return {
id: nextAttachmentId(),
name: file.name,
type: isImage ? 'image' : 'file',
mimeType: file.type || undefined,
previewUrl: isImage ? data : undefined,
data,
}
}
function readFileAsDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = () => reject(reader.error ?? new Error(`Failed to read ${file.name}`))
reader.readAsDataURL(file)
})
}