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 = {} ///strimg = JSON.stringify(img) var formats = clipboard.availableFormats(); var rawFilePath = clipboard.read('FileNameW'); if (rawFilePath) { var filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), ''); payload.type = 'filePath' payload.filePath = filePath console.log(filePath) } else if (formats.indexOf('image/png') > -1) { img = clipboard.readImage(); payload.type = 'dataURL' payload.dataURL = img.toDataURL().replace('png', 'gif') } else if (formats.indexOf('text/plain') > -1) { //potential link var potentialUrl = clipboard.readText() console.log("potentialUrl", potentialUrl) if (validateUrl(potentialUrl)) { payload.type = 'filePath' payload.filePath = potentialUrl } else { // paste as text element payload.type = 'text' payload.text = clipboard.readText() } } console.log(formats) //console.log(payload) 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('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) { // 只有一张图片,直接复制 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 = [] imageDataURLs.forEach((dataURL, index) => { try { const img = nativeImage.createFromDataURL(dataURL) if (!img.isEmpty()) { const filePath = path.join(tempDir, `image_${Date.now()}_${index + 1}.png`) fs.writeFileSync(filePath, img.toPNG()) filePaths.push(filePath) console.log('Saved temp image:', 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:', filePaths) // 清空剪贴板 clipboard.clear() // 方法1: 使用 HTML 格式(QQ 支持) const htmlImages = filePaths.map(fp => `` ).join('') const html = `${htmlImages}` // 方法2: 使用文件路径格式 const fileList = filePaths.join('\n') // 同时写入多种格式 clipboard.write({ text: fileList, html: html }) console.log('Copied', filePaths.length, 'images in multiple formats') } catch (err) { console.error('Error copying multiple images:', err) } }) ipcMain.on('copy-image', (event, imagePath) => { try { let img = null // 如果是 dataURL,直接写入 if (imagePath.startsWith('data:')) { img = nativeImage.createFromDataURL(imagePath) console.log('Processing dataURL image, size:', img.getSize()) // 确保图片不为空 if (img.isEmpty()) { console.error('Created image from dataURL is empty') clipboard.writeText('图片复制失败') return } } else 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 } img = nativeImage.createFromPath(filePath) console.log('Image loaded, size:', img.getSize()) } if (img && !img.isEmpty()) { // 清空剪贴板 clipboard.clear() // 写入图片到剪贴板 clipboard.writeImage(img) console.log('Successfully copied image to clipboard') console.log('Image size:', img.getSize()) console.log('Image aspect ratio:', img.getAspectRatio()) } else { console.log('Image is empty, copying error message') clipboard.writeText('图片为空,无法复制') } } 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); }