1039 lines
34 KiB
JavaScript
1039 lines
34 KiB
JavaScript
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);
|
||
} |