chore: add CONTRIBUTING.md for project guidelines and enhance README with author follow section

This commit is contained in:
Nexmoe
2025-10-24 19:22:37 +08:00
parent 8ad41eebe2
commit 4dfac6bb34
18 changed files with 808 additions and 580 deletions

View File

@@ -1,10 +1,15 @@
import { execFile } from 'node:child_process'
import type { Dirent } from 'node:fs'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { dialog, shell } from 'electron'
import { pathToFileURL } from 'node:url'
import { promisify } from 'node:util'
import { clipboard, dialog, shell } from 'electron'
import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'
const execFileAsync = promisify(execFile)
class FileSystemService extends IpcService {
static readonly groupName = 'fs'
@@ -109,10 +114,48 @@ class FileSystemService extends IpcService {
return false
}
@IpcMethod()
async copyFileToClipboard(_context: IpcContext, filePath: string): Promise<boolean> {
try {
if (!filePath) {
return false
}
const sanitizedPath = this.sanitizePath(filePath)
const normalizedPath = path.normalize(sanitizedPath)
const stats = await fs.stat(normalizedPath)
if (!stats.isFile()) {
return false
}
const resolvedPath = path.resolve(normalizedPath)
await this.copyFileToClipboardByPlatform(resolvedPath)
return true
} catch (error) {
console.error('Failed to copy file to clipboard:', error)
return false
}
}
private sanitizePath(target: string): string {
return target.trim().replace(/^['"]|['"]$/g, '')
}
private async copyFileToClipboardByPlatform(resolvedPath: string): Promise<void> {
switch (process.platform) {
case 'win32':
await this.copyFileToClipboardWindows(resolvedPath)
return
case 'darwin':
await this.copyFileToClipboardMac(resolvedPath)
return
default:
await this.copyFileToClipboardLinux(resolvedPath)
}
}
private async findLikelyFile(directory: string, expectedPath: string): Promise<string | null> {
try {
const dirStats = await fs.stat(directory)
@@ -171,6 +214,80 @@ class FileSystemService extends IpcService {
return false
}
}
private async copyFileToClipboardWindows(resolvedPath: string): Promise<void> {
const escaped = resolvedPath.replace(/'/g, "''")
try {
await execFileAsync('powershell.exe', [
'-NoLogo',
'-NoProfile',
'-Command',
`Set-Clipboard -Path '${escaped}'`
])
return
} catch (error) {
console.error('PowerShell clipboard copy failed, falling back to manual buffer:', error)
}
const winPath = resolvedPath.replace(/\//g, '\\')
const fileList = `${winPath}\u0000\u0000`
const encodedList = Buffer.from(fileList, 'ucs2')
const dropFilesStructSize = 20
const buffer = Buffer.alloc(dropFilesStructSize + encodedList.length)
buffer.writeUInt32LE(dropFilesStructSize, 0)
buffer.writeInt32LE(0, 4)
buffer.writeInt32LE(0, 8)
buffer.writeUInt32LE(0, 12)
buffer.writeUInt32LE(1, 16)
encodedList.copy(buffer, dropFilesStructSize)
clipboard.writeBuffer('CF_HDROP', buffer)
clipboard.writeBuffer('Preferred DropEffect', Buffer.from([1, 0, 0, 0]))
clipboard.writeBuffer('FileNameW', Buffer.from(`${path.basename(resolvedPath)}\u0000`, 'ucs2'))
clipboard.writeBuffer('FileName', Buffer.from(`${path.basename(resolvedPath)}\u0000`, 'ascii'))
}
private async copyFileToClipboardMac(resolvedPath: string): Promise<void> {
const escaped = resolvedPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
try {
await execFileAsync('osascript', ['-e', `set the clipboard to (POSIX file "${escaped}")`])
return
} catch (error) {
console.error('osascript clipboard copy failed, falling back to manual buffer:', error)
}
const entries = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
'<plist version="1.0">',
'<array>',
` <string>${this.escapeForPlist(resolvedPath)}</string>`,
'</array>',
'</plist>'
]
const plist = Buffer.from(entries.join('\n'), 'utf8')
clipboard.writeBuffer('NSFilenamesPboardType', plist)
const fileUrl = pathToFileURL(resolvedPath).toString()
clipboard.writeBuffer('public.file-url', Buffer.from(`${fileUrl}\n`, 'utf8'))
}
private async copyFileToClipboardLinux(resolvedPath: string): Promise<void> {
const fileUrl = pathToFileURL(resolvedPath).toString()
const content = `copy\n${fileUrl}`
clipboard.writeBuffer('x-special/gnome-copied-files', Buffer.from(content, 'utf8'))
clipboard.writeBuffer('text/uri-list', Buffer.from(`${fileUrl}\n`, 'utf8'))
}
private escapeForPlist(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
}
export { FileSystemService }