Files
pureRef/main.js
小煜 73aaab6d36 feat: 初始化RefVroid项目,添加核心功能与资源文件
- 添加主窗口、帮助窗口和系统托盘功能
- 实现图片、视频和文本的拖放与粘贴功能
- 添加场景保存与加载功能
- 包含基本UI样式和快捷键帮助文档
- 添加.gitattributes和.npmrc配置文件
- 包含LICENSE许可文件和图标资源
2025-12-17 21:00:07 +08:00

664 lines
19 KiB
JavaScript
Raw 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 = {}
///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 =>
`<img src="file:///${fp.replace(/\\/g, '/')}" />`
).join('')
const html = `<html><body>${htmlImages}</body></html>`
// 方法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);
}