Files
pureRef/main.js
小煜 ffb387bd20 feat: 实现撤销重做功能并优化剪贴板处理
添加撤销/重做历史记录功能,支持Ctrl+Z/Y/Shift+Z快捷键
优化剪贴板处理逻辑,支持GIF动画和多文件复制
更新帮助文档和图标资源,完善打包配置
2025-12-17 22:13:10 +08:00

1039 lines
34 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const {
BrowserWindow,
Menu,
MenuItem,
ipcMain,
app,
clipboard,
screen,
globalShortcut,
dialog,
Tray,
nativeImage
} = require('electron')
const path = require('path')
const fs = require('fs');
const Store = require('electron-store');
const store = new Store();
//store.clear()
let width = 400;
let height = 300;
let isLocked = false;
let tray = null;
let helpWindow = null;
function createWindow() {
const win = new BrowserWindow({
backgroundColor: "#202020",
width: width,
height: height,
frame: false,
transparent: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
}
})
win.setAlwaysOnTop(true);
win.loadFile('index.html')
return win;
}
function createHelpWindow(parentWindow) {
// 如果帮助窗口已存在,则聚焦它
if (helpWindow && !helpWindow.isDestroyed()) {
helpWindow.focus()
return
}
helpWindow = new BrowserWindow({
width: 700,
height: 600,
backgroundColor: "#2a2a2a",
title: "快捷键帮助 - RefVroid",
autoHideMenuBar: true,
parent: parentWindow,
modal: true,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
})
helpWindow.loadFile('help.html')
// 窗口关闭时清理引用
helpWindow.on('closed', () => {
helpWindow = null
})
}
app.commandLine.appendSwitch('disable-site-isolation-trials');
const contextMenu = new Menu()
const recentSubmenu = new Menu()
const windowSubmenu = new Menu()
const opacitySubmenu = new Menu()
app.whenReady().then(() => {
const mainWin = createWindow()
// 创建系统托盘图标
let trayIcon
const iconPath = path.join(__dirname, 'images', 'tray-icon.png')
if (fs.existsSync(iconPath)) {
trayIcon = nativeImage.createFromPath(iconPath)
} else {
// 使用 base64 图标
const iconData = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAA5SURBVDiN7dMxDQAgEMCwA/+fGQZgABZgARZgARZgARZgARZgARZgARZgARZgARZgARZgARbwsgMvpQYH8VLsJwAAAABJRU5ErkJggg=='
trayIcon = nativeImage.createFromDataURL(iconData)
}
if (trayIcon.isEmpty()) {
trayIcon = nativeImage.createFromBuffer(
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAA5SURBVDiN7dMxDQAgEMCwA/+fGQZgABZgARZgARZgARZgARZgARZgARZgARZgARZgARZgARbwsgMvpQYH8VLsJwAAAABJRU5ErkJggg==', 'base64')
)
}
tray = new Tray(trayIcon)
// 创建更新托盘菜单的函数
function updateTrayMenu() {
const menuTemplate = [
{
label: 'RefVroid',
enabled: false
},
{
type: 'separator'
}
]
// 只在锁定时显示解锁选项
if (isLocked) {
menuTemplate.push({
label: '解锁窗口',
click: () => {
isLocked = false
mainWin.setIgnoreMouseEvents(false)
mainWin.webContents.send('unlock-window')
updateTrayMenu()
}
})
}
menuTemplate.push(
{
label: '显示窗口',
click: () => {
mainWin.show()
mainWin.focus()
}
},
{
type: 'separator'
},
{
label: '退出',
click: () => {
app.quit()
}
}
)
const trayMenu = Menu.buildFromTemplate(menuTemplate)
tray.setContextMenu(trayMenu)
}
updateTrayMenu()
tray.setToolTip('RefVroid - 动画参考工具')
tray.on('click', () => {
mainWin.isVisible() ? mainWin.hide() : mainWin.show()
})
mainWin.on('ready-to-show', () => {
console.log('Window ready to be presented');
mainWin.show();
});
mainWin.webContents.on('did-finish-load', () => {
console.log('Page fully loaded');
//setTimeout(loadMostRecent, 1000)
loadMostRecent()
});
contextMenu.append(new MenuItem({
label: '粘贴',
accelerator: process.platform === 'darwin' ? 'Cmd+V' : 'Ctrl+V',
click: (menuItem, browserWindow, event) => {
handlePaste();
}
}));
contextMenu.append(new MenuItem({ type: 'separator' }))
contextMenu.append(new MenuItem({
label: '总是置顶', type: 'checkbox', checked: true,
click: (menuItem, browserWindow, event) => {
mainWin.setAlwaysOnTop(menuItem.checked);
}
}));
// 只注册开发者工具的全局快捷键
globalShortcut.register('Control+Shift+I', () => {
mainWin.webContents.openDevTools()
});
function handlePaste() {
console.log('========== handlePaste ==========')
let payload = {}
var formats = clipboard.availableFormats();
console.log('Available clipboard formats:', formats)
// 尝试读取所有可能的格式
console.log('Trying FileNameW...')
var rawFilePath = clipboard.read('FileNameW');
console.log('FileNameW result:', rawFilePath ? 'found' : 'not found')
// 尝试读取 HTML 格式(可能包含文件路径)
if (!rawFilePath && formats.indexOf('text/html') > -1) {
try {
const html = clipboard.readHTML();
console.log('HTML content:', html.substring(0, 200));
// 尝试从 HTML 中提取文件路径
const srcMatch = html.match(/src=["']file:\/\/\/([^"']+)["']/i);
if (srcMatch) {
rawFilePath = srcMatch[1].replace(/\//g, '\\');
console.log('Extracted file path from HTML:', rawFilePath);
}
} catch (err) {
console.log('Failed to read HTML:', err.message);
}
}
if (rawFilePath) {
var filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '');
// 检查是否是图片文件,如果是则转换为 dataURL
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'];
const isImage = imageExtensions.some(ext => filePath.toLowerCase().endsWith(ext));
if (isImage && fs.existsSync(filePath)) {
try {
// 读取文件并转换为 base64
const imageBuffer = fs.readFileSync(filePath);
const base64 = imageBuffer.toString('base64');
// 根据文件扩展名确定 MIME 类型
let mimeType = 'image/png';
if (filePath.toLowerCase().endsWith('.jpg') || filePath.toLowerCase().endsWith('.jpeg')) {
mimeType = 'image/jpeg';
} else if (filePath.toLowerCase().endsWith('.gif')) {
mimeType = 'image/gif';
} else if (filePath.toLowerCase().endsWith('.bmp')) {
mimeType = 'image/bmp';
} else if (filePath.toLowerCase().endsWith('.webp')) {
mimeType = 'image/webp';
}
payload.type = 'dataURL';
payload.dataURL = `data:${mimeType};base64,${base64}`;
console.log('Converted file to dataURL:', filePath, mimeType);
} catch (err) {
console.error('Error converting file to dataURL:', err);
payload.type = 'filePath';
payload.filePath = filePath;
}
} else {
payload.type = 'filePath';
payload.filePath = filePath;
}
console.log(filePath)
} else {
// 尝试读取原始图片数据(支持 GIF 动画)
let imageBuffer = null;
let mimeType = null;
console.log('No file path found, trying to read image data...')
// 尝试所有可能的图片格式
const imageFormats = [
'image/gif',
'image/png',
'image/jpeg',
'image/jpg',
'image/bmp',
'image/webp',
'CF_DIB', // Windows DIB format
'CF_DIBV5' // Windows DIB v5 format
];
for (const format of imageFormats) {
if (formats.indexOf(format) > -1) {
try {
console.log(`Trying to read format: ${format}`)
const buffer = clipboard.readBuffer(format);
if (buffer && buffer.length > 0) {
imageBuffer = buffer;
// 检测实际的图片类型(通过文件头)
const header = buffer.slice(0, 6).toString('hex');
console.log(`Buffer header: ${header}, size: ${buffer.length}`)
if (header.startsWith('474946')) {
mimeType = 'image/gif';
console.log('Detected GIF by header');
} else if (header.startsWith('89504e')) {
mimeType = 'image/png';
console.log('Detected PNG by header');
} else if (header.startsWith('ffd8ff')) {
mimeType = 'image/jpeg';
console.log('Detected JPEG by header');
} else {
// 使用格式名称作为后备
mimeType = format.startsWith('image/') ? format : 'image/png';
console.log('Using format name as mime type:', mimeType);
}
break;
}
} catch (err) {
console.log(`Failed to read ${format}:`, err.message);
}
}
}
if (imageBuffer && imageBuffer.length > 0) {
// 成功读取到原始图片数据
const base64 = imageBuffer.toString('base64');
payload.type = 'dataURL';
payload.dataURL = `data:${mimeType};base64,${base64}`;
console.log('✓ Successfully converted clipboard image to dataURL');
console.log(' Type:', mimeType);
console.log(' Size:', imageBuffer.length, 'bytes');
console.log(' Base64 length:', base64.length);
} else if (formats.indexOf('image/png') > -1 || formats.some(f => f.startsWith('image/'))) {
// 降级方案:使用 readImage会丢失 GIF 动画)
console.log('⚠ Using fallback readImage (will lose GIF animation)');
img = clipboard.readImage();
payload.type = 'dataURL';
payload.dataURL = img.toDataURL();
console.log(' Fallback image size:', img.getSize());
} else if (formats.indexOf('text/plain') > -1) {
// 文本内容
var potentialUrl = clipboard.readText();
console.log("Checking if text is URL:", potentialUrl);
if (validateUrl(potentialUrl)) {
payload.type = 'filePath';
payload.filePath = potentialUrl;
console.log('✓ Detected URL');
} else {
// paste as text element
payload.type = 'text';
payload.text = clipboard.readText();
console.log('✓ Pasting as text');
}
} else {
console.log('✗ No supported format found in clipboard');
}
}
console.log('========== Final payload type:', payload.type, '==========')
mainWin.webContents.send('clipboard', JSON.stringify(payload)) // send to web page
}
function addToRecent(filePath) {
var recent = JSON.parse(store.get('recent') || "[]")
console.log("addToRecent", store.get('recent'), filePath)
let index = recent.indexOf(filePath)
let exists = index != -1
if (exists) {
recent.splice(index, 1)
}
recent.push(filePath)
store.set('recent', JSON.stringify(recent));
console.log("addedToRecent", store.get('recent'), filePath)
if (!exists) {
recentSubmenu.append(new MenuItem({
label: filePath,
click: (menuItem, browserWindow, event) => {
readAndLoadFilePath(menuItem.label)
}
}))
}
}
function populateRecent() {
var recent = JSON.parse(store.get('recent') || "[]")
console.log("populateRecent", store.get('recent'))
for (recentfile of recent) {
recentSubmenu.append(new MenuItem({
label: recentfile,
click: (menuItem, browserWindow, event) => {
readAndLoadFilePath(menuItem.label)
}
}))
}
}
function loadMostRecent() {
var recent = JSON.parse(store.get('recent') || "[]")
if (recent.length > 0)
readAndLoadFilePath(recent[recent.length - 1])
}
contextMenu.append(new MenuItem({ type: 'separator' }))
function readAndLoadFilePath(filePath) {
fs.readFile(filePath, (err, data) => {
if (err) throw err;
let newState = JSON.parse(data);
mainWin.webContents.send('load-scene', newState, filePath)
});
}
contextMenu.append(new MenuItem({
label: "最近使用", type: 'submenu',
submenu: recentSubmenu
}));
populateRecent()
// 添加窗口菜单
contextMenu.append(new MenuItem({
label: "窗口", type: 'submenu',
submenu: windowSubmenu
}));
// 添加透明度菜单
for (let opacity = 100; opacity >= 10; opacity -= 10) {
opacitySubmenu.append(new MenuItem({
label: `${opacity}%`,
click: (menuItem, browserWindow, event) => {
mainWin.setOpacity(opacity / 100)
}
}))
}
contextMenu.append(new MenuItem({
label: "透明度", type: 'submenu',
submenu: opacitySubmenu
}));
// 添加锁定窗口菜单
contextMenu.append(new MenuItem({
id: "lock-window",
label: '锁定窗口',
click: (menuItem, browserWindow, event) => {
isLocked = true
mainWin.setIgnoreMouseEvents(true, { forward: true })
mainWin.webContents.send('lock-window')
updateTrayMenu()
}
}));
contextMenu.append(new MenuItem({
label: '加载',
accelerator: process.platform === 'darwin' ? 'Cmd+L' : 'Ctrl+L',
click: (menuItem, browserWindow, event) => {
dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'RefVroid 场景', extensions: ['purgif'] }
]
}).then(result => {
if (!result.canceled) {
readAndLoadFilePath(result.filePaths[0])
}
}).catch(err => {
console.log(err)
})
}
}));
contextMenu.append(new MenuItem({
label: '保存',
accelerator: process.platform === 'darwin' ? 'Cmd+S' : 'Ctrl+S',
click: (menuItem, browserWindow, event) => {
dialog.showSaveDialog({
defaultPath: 'scene.purgif',
filters: [
{ name: 'RefVroid 场景', extensions: ['purgif'] }
]
}).then(result => {
if (!result.canceled) {
mainWin.webContents.send('save-scene', result.filePath)
addToRecent(result.filePath)
}
}).catch(err => {
console.log(err)
})
}
}));
contextMenu.append(new MenuItem({
label: '新建场景',
accelerator: process.platform === 'darwin' ? 'Cmd+N' : 'Ctrl+N',
click: (menuItem, browserWindow, event) => {
mainWin.webContents.send('new-scene');
}
}));
contextMenu.append(new MenuItem({
label: '关闭',
accelerator: process.platform === 'darwin' ? 'Cmd+W' : 'Ctrl+W',
click: (menuItem, browserWindow, event) => {
browserWindow.close();
}
}));
Menu.setApplicationMenu(contextMenu)
if (process.argv.indexOf("debug") > -1)
mainWin.webContents.openDevTools()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
app.on('browser-window-created', (event, win) => {
win.webContents.on('context-menu', (e, params) => {
//menu.popup(win, params.x, params.y)
})
})
ipcMain.on('ready', (event, menuType) => {
//loadMostRecent()
windowIsReady = true;
});
ipcMain.on('show-context-menu', (event, menuType) => {
const win = BrowserWindow.fromWebContents(event.sender)
// 根据窗口状态动态更新窗口菜单
const isMaximized = win.isMaximized()
windowSubmenu.clear()
windowSubmenu.append(new MenuItem({
label: isMaximized ? "恢复默认" : "最大化",
accelerator: process.platform === 'darwin' ? 'Cmd+F' : 'Ctrl+F',
click: (menuItem, browserWindow, event) => {
if(!browserWindow.isMaximized()) {
browserWindow.maximize();
} else {
browserWindow.unmaximize();
}
}
}))
windowSubmenu.append(new MenuItem({
label: "最小化",
accelerator: process.platform === 'darwin' ? 'Cmd+M' : 'Ctrl+M',
click: (menuItem, browserWindow, event) => {
browserWindow.minimize();
}
}))
contextMenu.popup(win)
})
ipcMain.on('save-scene', (event, filePath, stateCopy) => {
console.log('save', filePath, stateCopy)
let data = JSON.stringify(stateCopy);
fs.writeFileSync(filePath, data);
//const win = BrowserWindow.fromWebContents(event.sender)
//menu.popup(win)
})
let dragState = {
dragging: false
}
ipcMain.on('handle-paste', (event, w, h) => {
handlePaste()
})
ipcMain.on('trigger-load', (event) => {
dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'RefVroid 场景', extensions: ['purgif'] }
]
}).then(result => {
if (!result.canceled) {
readAndLoadFilePath(result.filePaths[0])
}
}).catch(err => {
console.log(err)
})
})
ipcMain.on('trigger-save', (event) => {
dialog.showSaveDialog({
defaultPath: 'scene.purgif',
filters: [
{ name: 'RefVroid 场景', extensions: ['purgif'] }
]
}).then(result => {
if (!result.canceled) {
mainWin.webContents.send('save-scene', result.filePath)
addToRecent(result.filePath)
}
}).catch(err => {
console.log(err)
})
})
ipcMain.on('trigger-close', (event) => {
mainWin.close()
})
ipcMain.on('trigger-maximize', (event) => {
if(!mainWin.isMaximized()) {
mainWin.maximize()
} else {
mainWin.unmaximize()
}
})
ipcMain.on('trigger-minimize', (event) => {
mainWin.minimize()
})
ipcMain.on('show-help', (event) => {
createHelpWindow(mainWin)
})
ipcMain.on('convert-file-to-dataurl', (event, filePath) => {
try {
console.log('Converting file to dataURL:', filePath)
// 读取文件
const imageBuffer = fs.readFileSync(filePath)
const base64 = imageBuffer.toString('base64')
// 根据文件扩展名确定 MIME 类型
let mimeType = 'image/png'
const lowerPath = filePath.toLowerCase()
if (lowerPath.endsWith('.jpg') || lowerPath.endsWith('.jpeg')) {
mimeType = 'image/jpeg'
} else if (lowerPath.endsWith('.gif')) {
mimeType = 'image/gif'
} else if (lowerPath.endsWith('.bmp')) {
mimeType = 'image/bmp'
} else if (lowerPath.endsWith('.webp')) {
mimeType = 'image/webp'
}
const dataURL = `data:${mimeType};base64,${base64}`
console.log('Converted to dataURL, type:', mimeType)
// 发送回渲染进程
event.sender.send('file-converted-to-dataurl', dataURL)
} catch (err) {
console.error('Error converting file to dataURL:', err)
}
})
ipcMain.on('copy-text', (event, text) => {
clipboard.writeText(text)
console.log('Copied text to clipboard:', text)
})
ipcMain.on('copy-multiple-images', (event, imageDataURLs) => {
try {
console.log('Received', imageDataURLs.length, 'images to copy')
if (imageDataURLs.length === 1) {
// 只有一张图片,使用 copy-image 的逻辑(支持 GIF
console.log('Single image, using copy-image handler')
// 触发 copy-image 事件
mainWin.webContents.send('trigger-copy-single-image')
// 直接调用 copy-image 的逻辑
const imagePath = imageDataURLs[0]
// 检测是否是 GIF
if (imagePath.startsWith('data:')) {
const base64Data = imagePath.split(',')[1]
const buffer = Buffer.from(base64Data, 'base64')
const header = buffer.slice(0, 6).toString('hex')
if (header.startsWith('474946')) {
// 是 GIF保存为临时文件
console.log('Detected GIF in multiple selection')
const tempDir = path.join(app.getPath('temp'), 'RefVroid-clipboard')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
const tempFilePath = path.join(tempDir, `animated_${Date.now()}.gif`)
fs.writeFileSync(tempFilePath, buffer)
clipboard.clear()
const filePathBuffer = Buffer.from(tempFilePath + '\0', 'ucs2')
clipboard.writeBuffer('FileNameW', filePathBuffer)
console.log('✓ Copied GIF from multiple selection')
return
}
}
// 不是 GIF使用普通方法
const img = nativeImage.createFromDataURL(imageDataURLs[0])
if (!img.isEmpty()) {
clipboard.clear()
clipboard.writeImage(img)
console.log('Copied single image')
}
return
}
// 创建临时文件夹
const tempDir = path.join(app.getPath('temp'), 'RefVroid-copy')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
// 清理旧文件
try {
const oldFiles = fs.readdirSync(tempDir)
oldFiles.forEach(file => {
try {
fs.unlinkSync(path.join(tempDir, file))
} catch (err) {
// 忽略删除错误
}
})
} catch (err) {
// 忽略读取目录错误
}
// 保存所有图片为临时文件
const filePaths = []
const timestamp = Date.now()
imageDataURLs.forEach((dataURL, index) => {
try {
// 检测图片类型
const base64Data = dataURL.split(',')[1]
const buffer = Buffer.from(base64Data, 'base64')
const header = buffer.slice(0, 6).toString('hex')
let extension = 'png'
let fileBuffer = null
if (header.startsWith('474946')) {
// GIF - 保存原始数据
extension = 'gif'
fileBuffer = buffer
console.log(' Image', index + 1, 'is GIF, preserving animation')
} else {
// 其他格式 - 转换为 PNG
const img = nativeImage.createFromDataURL(dataURL)
if (!img.isEmpty()) {
fileBuffer = img.toPNG()
console.log(' Image', index + 1, 'converted to PNG')
}
}
if (fileBuffer) {
const filePath = path.join(tempDir, `image_${timestamp}_${index + 1}.${extension}`)
fs.writeFileSync(filePath, fileBuffer)
filePaths.push(filePath)
console.log(' Saved:', filePath)
}
} catch (err) {
console.error('Error saving image', index, ':', err)
}
})
if (filePaths.length === 0) {
console.log('No valid images to copy')
return
}
console.log('Copying', filePaths.length, 'files')
// 清空剪贴板
clipboard.clear()
// 方法1: 使用 Windows 文件格式FileNameW
// 这是 Windows 文件复制的标准格式,大多数应用都支持
try {
// 构建文件列表(每个路径以 null 结尾,最后再加一个 null
let fileListBuffer = Buffer.alloc(0)
filePaths.forEach(fp => {
const pathBuffer = Buffer.from(fp + '\0', 'ucs2')
fileListBuffer = Buffer.concat([fileListBuffer, pathBuffer])
})
// 添加最后的 null 终止符
fileListBuffer = Buffer.concat([fileListBuffer, Buffer.from('\0', 'ucs2')])
clipboard.writeBuffer('FileNameW', fileListBuffer)
console.log(' Wrote FileNameW format')
} catch (err) {
console.error('Failed to write FileNameW:', err)
}
// 方法2: 使用 HTML 格式(某些应用支持)
try {
const htmlImages = filePaths.map(fp =>
`<img src="file:///${fp.replace(/\\/g, '/')}" />`
).join('')
const html = `<html><body>${htmlImages}</body></html>`
clipboard.writeHTML(html)
console.log(' Wrote HTML format')
} catch (err) {
console.error('Failed to write HTML:', err)
}
// 方法3: 如果只有一张图片,同时写入图片数据(提高兼容性)
if (filePaths.length === 1) {
try {
const buffer = fs.readFileSync(filePaths[0])
const img = nativeImage.createFromBuffer(buffer)
if (!img.isEmpty()) {
clipboard.writeImage(img)
console.log(' Also wrote image data for single image')
}
} catch (err) {
console.error('Failed to write image data:', err)
}
}
console.log('✓ Copied', filePaths.length, 'file(s) in multiple formats')
console.log(' Note: Some apps (like Feishu) may show file paths instead of images')
} catch (err) {
console.error('Error copying multiple images:', err)
}
})
ipcMain.on('copy-image', (event, imagePath) => {
try {
console.log('========== copy-image ==========')
console.log('Image path/dataURL:', imagePath.substring(0, 100))
// 如果是 dataURL检测真实的图片类型
if (imagePath.startsWith('data:')) {
try {
// 从 dataURL 提取 base64 数据
const base64Data = imagePath.split(',')[1]
const buffer = Buffer.from(base64Data, 'base64')
// 检测文件头来确定真实类型
const header = buffer.slice(0, 6).toString('hex')
console.log('Image buffer header:', header, 'size:', buffer.length, 'bytes')
let actualMimeType = null
let isGif = false
if (header.startsWith('474946')) {
// GIF 文件头: 47 49 46 = "GIF"
actualMimeType = 'image/gif'
isGif = true
console.log('✓ Detected GIF by file header')
} else if (header.startsWith('89504e')) {
// PNG 文件头: 89 50 4E 47
actualMimeType = 'image/png'
console.log('✓ Detected PNG by file header')
} else if (header.startsWith('ffd8ff')) {
// JPEG 文件头: FF D8 FF
actualMimeType = 'image/jpeg'
console.log('✓ Detected JPEG by file header')
} else {
console.log('⚠ Unknown image format, using declared MIME type')
actualMimeType = imagePath.split(';')[0].split(':')[1]
}
if (isGif) {
// GIF - 保存为临时文件并复制文件
console.log('Copying GIF with animation...')
try {
// 创建临时文件
const tempDir = path.join(app.getPath('temp'), 'RefVroid-clipboard')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
// 清理旧的临时文件
try {
const files = fs.readdirSync(tempDir)
const now = Date.now()
files.forEach(file => {
const filePath = path.join(tempDir, file)
const stats = fs.statSync(filePath)
// 删除超过1小时的文件
if (now - stats.mtimeMs > 3600000) {
fs.unlinkSync(filePath)
}
})
} catch (err) {
// 忽略清理错误
}
const tempFilePath = path.join(tempDir, `animated_${Date.now()}.gif`)
fs.writeFileSync(tempFilePath, buffer)
console.log(' Created temp GIF file:', tempFilePath)
// 使用 Electron 的 clipboard.writeBuffer 写入文件路径
// 这是 Windows 文件复制的标准格式
clipboard.clear()
// 写入文件路径CF_HDROP 格式)
const filePathBuffer = Buffer.from(tempFilePath + '\0', 'ucs2')
clipboard.writeBuffer('FileNameW', filePathBuffer)
console.log('✓ Successfully copied GIF file to clipboard')
console.log(' Temp file:', tempFilePath)
console.log(' You can now paste it to QQ or other apps')
return
} catch (err) {
console.error('Failed to copy GIF with temp file:', err)
// 降级方案:只写入静态图片
clipboard.clear()
const img = nativeImage.createFromBuffer(buffer)
if (!img.isEmpty()) {
clipboard.writeImage(img)
console.log('⚠ Copied as static image (animation lost)')
return
}
}
} else {
// 其他格式 - 使用 nativeImage
console.log('Copying', actualMimeType, 'image...')
const correctedDataURL = `data:${actualMimeType};base64,${base64Data}`
const img = nativeImage.createFromDataURL(correctedDataURL)
if (!img.isEmpty()) {
clipboard.clear()
clipboard.writeImage(img)
console.log('✓ Successfully copied', actualMimeType, 'to clipboard')
console.log(' Image size:', img.getSize())
return
} else {
console.error('✗ Created image is empty')
}
}
} catch (err) {
console.error('Failed to process dataURL:', err)
clipboard.writeText('图片复制失败: ' + err.message)
return
}
}
// 处理网络图片
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
// 网络图片暂时只能复制 URL
clipboard.writeText(imagePath)
console.log('Copied image URL to clipboard:', imagePath)
return
} else {
// 本地文件路径
// 处理可能的 file:// 协议
let filePath = imagePath
if (filePath.startsWith('file://')) {
filePath = filePath.substring(7)
}
// Windows 路径处理
if (process.platform === 'win32' && filePath.startsWith('/')) {
filePath = filePath.substring(1)
}
console.log('Reading image from path:', filePath)
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
console.error('File does not exist:', filePath)
clipboard.writeText('文件不存在: ' + filePath)
return
}
// 检查是否是 GIF 文件
const isGifFile = filePath.toLowerCase().endsWith('.gif')
if (isGifFile) {
// GIF 文件 - 直接复制文件路径
try {
console.log('Detected GIF file, copying file path')
const buffer = fs.readFileSync(filePath)
console.log('GIF buffer size:', buffer.length, 'bytes')
clipboard.clear()
// 写入文件路径Windows 格式)
clipboard.write({
text: filePath,
bookmark: filePath
})
// 同时写入 GIF buffer
clipboard.writeBuffer('image/gif', buffer)
// 写入静态图片作为降级
const img = nativeImage.createFromBuffer(buffer)
if (!img.isEmpty()) {
clipboard.writeImage(img)
console.log(' Also wrote static image as fallback')
}
console.log('✓ Successfully copied GIF file')
return
} catch (err) {
console.error('Failed to copy GIF file:', err)
// 降级到普通方法
}
}
// 其他格式的文件
const img = nativeImage.createFromPath(filePath)
console.log('Image loaded, size:', img.getSize())
if (!img.isEmpty()) {
clipboard.clear()
clipboard.writeImage(img)
console.log('✓ Successfully copied image to clipboard')
console.log(' Image size:', img.getSize())
return
} else {
console.error('✗ Image is empty')
clipboard.writeText('图片为空')
return
}
}
} catch (err) {
console.error('Error copying image:', err)
clipboard.writeText('复制图片时出错: ' + err.message)
}
})
ipcMain.on('loaded-state', (event, filePath) => {
addToRecent(filePath)
})
ipcMain.on('record-window-size', (event, w, h) => {
width = mainWin.getSize()[0]
height = mainWin.getSize()[1]
})
ipcMain.on('move-electron-window', (event, x, y, initPos) => {
//var display = screen.getDisplayNearestPoint({x: x, y: y})
//var dpiRespected = screen.dipToScreenPoint({x: x, y: y})
//mainWin.setPosition(x,y)
mainWin.setBounds({
width: width,
height: height,
x: x - initPos.x,
y: y - initPos.y
});
//win.setSize(width, height)
//mainWin.setPosition(Math.round(x / 1.25) - Math.round(initPos.x / 1.25), Math.round(y / 1.25) - Math.round(initPos.y / 1.25))
})
let loopToLoad = function loopToLoad(){
if(windowIsReady){
console.log("loadMostRecent")
//setTimeout(loadMostRecent, 700)
//loadMostRecent()
}
else{
console.log("not ready")
setTimeout(loopToLoad, 100)
}
};
loopToLoad();
})
let windowIsReady = false;
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('will-quit', () => {
globalShortcut.unregisterAll()
})
function validateUrl(value) {
return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(value);
}