feat: 实现撤销重做功能并优化剪贴板处理
添加撤销/重做历史记录功能,支持Ctrl+Z/Y/Shift+Z快捷键 优化剪贴板处理逻辑,支持GIF动画和多文件复制 更新帮助文档和图标资源,完善打包配置
This commit is contained in:
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>快捷键帮助 - AnimRef</title>
|
<title>快捷键帮助 - RefVroid</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -99,6 +99,9 @@
|
|||||||
<h3>文件操作</h3>
|
<h3>文件操作</h3>
|
||||||
<div class="help-item"><kbd>Ctrl+C</kbd><span>复制选中元素</span></div>
|
<div class="help-item"><kbd>Ctrl+C</kbd><span>复制选中元素</span></div>
|
||||||
<div class="help-item"><kbd>Ctrl+V</kbd><span>粘贴图片/文本</span></div>
|
<div class="help-item"><kbd>Ctrl+V</kbd><span>粘贴图片/文本</span></div>
|
||||||
|
<div class="help-item"><kbd>Ctrl+Z</kbd><span>撤销</span></div>
|
||||||
|
<div class="help-item"><kbd>Ctrl+Y</kbd><span>重做</span></div>
|
||||||
|
<div class="help-item"><kbd>Ctrl+Shift+Z</kbd><span>重做(备选)</span></div>
|
||||||
<div class="help-item"><kbd>Ctrl+L</kbd><span>加载场景</span></div>
|
<div class="help-item"><kbd>Ctrl+L</kbd><span>加载场景</span></div>
|
||||||
<div class="help-item"><kbd>Ctrl+S</kbd><span>保存场景</span></div>
|
<div class="help-item"><kbd>Ctrl+S</kbd><span>保存场景</span></div>
|
||||||
<div class="help-item"><kbd>Ctrl+N</kbd><span>新建场景</span></div>
|
<div class="help-item"><kbd>Ctrl+N</kbd><span>新建场景</span></div>
|
||||||
|
|||||||
BIN
images/icon.ico
Normal file
BIN
images/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
images/icon.png
Normal file
BIN
images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 180 B After Width: | Height: | Size: 131 KiB |
483
main.js
483
main.js
@@ -185,39 +185,162 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handlePaste() {
|
function handlePaste() {
|
||||||
console.log('handlePaste')
|
console.log('========== handlePaste ==========')
|
||||||
|
|
||||||
let payload = {}
|
let payload = {}
|
||||||
///strimg = JSON.stringify(img)
|
|
||||||
var formats = clipboard.availableFormats();
|
var formats = clipboard.availableFormats();
|
||||||
|
console.log('Available clipboard formats:', formats)
|
||||||
|
|
||||||
|
// 尝试读取所有可能的格式
|
||||||
|
console.log('Trying FileNameW...')
|
||||||
var rawFilePath = clipboard.read('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) {
|
if (rawFilePath) {
|
||||||
var filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '');
|
var filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '');
|
||||||
payload.type = 'filePath'
|
|
||||||
payload.filePath = filePath
|
// 检查是否是图片文件,如果是则转换为 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)
|
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 {
|
} else {
|
||||||
// paste as text element
|
// 尝试读取原始图片数据(支持 GIF 动画)
|
||||||
payload.type = 'text'
|
let imageBuffer = null;
|
||||||
payload.text = clipboard.readText()
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(formats)
|
if (imageBuffer && imageBuffer.length > 0) {
|
||||||
//console.log(payload)
|
// 成功读取到原始图片数据
|
||||||
|
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
|
mainWin.webContents.send('clipboard', JSON.stringify(payload)) // send to web page
|
||||||
}
|
}
|
||||||
function addToRecent(filePath) {
|
function addToRecent(filePath) {
|
||||||
@@ -464,6 +587,36 @@ app.whenReady().then(() => {
|
|||||||
ipcMain.on('show-help', (event) => {
|
ipcMain.on('show-help', (event) => {
|
||||||
createHelpWindow(mainWin)
|
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) => {
|
ipcMain.on('copy-text', (event, text) => {
|
||||||
clipboard.writeText(text)
|
clipboard.writeText(text)
|
||||||
console.log('Copied text to clipboard:', text)
|
console.log('Copied text to clipboard:', text)
|
||||||
@@ -473,7 +626,40 @@ app.whenReady().then(() => {
|
|||||||
console.log('Received', imageDataURLs.length, 'images to copy')
|
console.log('Received', imageDataURLs.length, 'images to copy')
|
||||||
|
|
||||||
if (imageDataURLs.length === 1) {
|
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])
|
const img = nativeImage.createFromDataURL(imageDataURLs[0])
|
||||||
if (!img.isEmpty()) {
|
if (!img.isEmpty()) {
|
||||||
clipboard.clear()
|
clipboard.clear()
|
||||||
@@ -505,14 +691,37 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
// 保存所有图片为临时文件
|
// 保存所有图片为临时文件
|
||||||
const filePaths = []
|
const filePaths = []
|
||||||
|
const timestamp = Date.now()
|
||||||
|
|
||||||
imageDataURLs.forEach((dataURL, index) => {
|
imageDataURLs.forEach((dataURL, index) => {
|
||||||
try {
|
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)
|
const img = nativeImage.createFromDataURL(dataURL)
|
||||||
if (!img.isEmpty()) {
|
if (!img.isEmpty()) {
|
||||||
const filePath = path.join(tempDir, `image_${Date.now()}_${index + 1}.png`)
|
fileBuffer = img.toPNG()
|
||||||
fs.writeFileSync(filePath, 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)
|
filePaths.push(filePath)
|
||||||
console.log('Saved temp image:', filePath)
|
console.log(' Saved:', filePath)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving image', index, ':', err)
|
console.error('Error saving image', index, ':', err)
|
||||||
@@ -524,27 +733,58 @@ app.whenReady().then(() => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Copying', filePaths.length, 'files:', filePaths)
|
console.log('Copying', filePaths.length, 'files')
|
||||||
|
|
||||||
// 清空剪贴板
|
// 清空剪贴板
|
||||||
clipboard.clear()
|
clipboard.clear()
|
||||||
|
|
||||||
// 方法1: 使用 HTML 格式(QQ 支持)
|
// 方法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 =>
|
const htmlImages = filePaths.map(fp =>
|
||||||
`<img src="file:///${fp.replace(/\\/g, '/')}" />`
|
`<img src="file:///${fp.replace(/\\/g, '/')}" />`
|
||||||
).join('')
|
).join('')
|
||||||
const html = `<html><body>${htmlImages}</body></html>`
|
const html = `<html><body>${htmlImages}</body></html>`
|
||||||
|
|
||||||
// 方法2: 使用文件路径格式
|
clipboard.writeHTML(html)
|
||||||
const fileList = filePaths.join('\n')
|
console.log(' Wrote HTML format')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to write HTML:', err)
|
||||||
|
}
|
||||||
|
|
||||||
// 同时写入多种格式
|
// 方法3: 如果只有一张图片,同时写入图片数据(提高兼容性)
|
||||||
clipboard.write({
|
if (filePaths.length === 1) {
|
||||||
text: fileList,
|
try {
|
||||||
html: html
|
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, 'images in multiple formats')
|
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) {
|
} catch (err) {
|
||||||
console.error('Error copying multiple images:', err)
|
console.error('Error copying multiple images:', err)
|
||||||
@@ -552,20 +792,121 @@ app.whenReady().then(() => {
|
|||||||
})
|
})
|
||||||
ipcMain.on('copy-image', (event, imagePath) => {
|
ipcMain.on('copy-image', (event, imagePath) => {
|
||||||
try {
|
try {
|
||||||
let img = null
|
console.log('========== copy-image ==========')
|
||||||
|
console.log('Image path/dataURL:', imagePath.substring(0, 100))
|
||||||
|
|
||||||
// 如果是 dataURL,直接写入
|
// 如果是 dataURL,检测真实的图片类型
|
||||||
if (imagePath.startsWith('data:')) {
|
if (imagePath.startsWith('data:')) {
|
||||||
img = nativeImage.createFromDataURL(imagePath)
|
try {
|
||||||
console.log('Processing dataURL image, size:', img.getSize())
|
// 从 dataURL 提取 base64 数据
|
||||||
|
const base64Data = imagePath.split(',')[1]
|
||||||
|
const buffer = Buffer.from(base64Data, 'base64')
|
||||||
|
|
||||||
// 确保图片不为空
|
// 检测文件头来确定真实类型
|
||||||
if (img.isEmpty()) {
|
const header = buffer.slice(0, 6).toString('hex')
|
||||||
console.error('Created image from dataURL is empty')
|
console.log('Image buffer header:', header, 'size:', buffer.length, 'bytes')
|
||||||
clipboard.writeText('图片复制失败')
|
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
} else if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
}
|
||||||
|
} 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
|
// 网络图片暂时只能复制 URL
|
||||||
clipboard.writeText(imagePath)
|
clipboard.writeText(imagePath)
|
||||||
console.log('Copied image URL to clipboard:', imagePath)
|
console.log('Copied image URL to clipboard:', imagePath)
|
||||||
@@ -591,23 +932,57 @@ app.whenReady().then(() => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
img = nativeImage.createFromPath(filePath)
|
// 检查是否是 GIF 文件
|
||||||
console.log('Image loaded, size:', img.getSize())
|
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')
|
||||||
|
|
||||||
if (img && !img.isEmpty()) {
|
|
||||||
// 清空剪贴板
|
|
||||||
clipboard.clear()
|
clipboard.clear()
|
||||||
|
|
||||||
// 写入图片到剪贴板
|
// 写入文件路径(Windows 格式)
|
||||||
clipboard.writeImage(img)
|
clipboard.write({
|
||||||
|
text: filePath,
|
||||||
|
bookmark: filePath
|
||||||
|
})
|
||||||
|
|
||||||
console.log('Successfully copied image to clipboard')
|
// 同时写入 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())
|
console.log(' Image size:', img.getSize())
|
||||||
console.log('Image aspect ratio:', img.getAspectRatio())
|
return
|
||||||
} else {
|
} else {
|
||||||
console.log('Image is empty, copying error message')
|
console.error('✗ Image is empty')
|
||||||
clipboard.writeText('图片为空,无法复制')
|
clipboard.writeText('图片为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error copying image:', err)
|
console.error('Error copying image:', err)
|
||||||
|
|||||||
1202
package-lock.json
generated
1202
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -14,13 +14,14 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.4.0",
|
"@electron-forge/cli": "^7.4.0",
|
||||||
"@electron-forge/maker-deb": "^7.4.0",
|
"@electron-forge/maker-deb": "^7.4.0",
|
||||||
|
"@electron-forge/maker-dmg": "^7.4.0",
|
||||||
|
"@electron-forge/maker-flatpak": "^7.4.0",
|
||||||
"@electron-forge/maker-rpm": "^7.4.0",
|
"@electron-forge/maker-rpm": "^7.4.0",
|
||||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
"@electron-forge/maker-squirrel": "^7.4.0",
|
||||||
|
"@electron-forge/maker-wix": "^7.10.2",
|
||||||
"@electron-forge/maker-zip": "^7.4.0",
|
"@electron-forge/maker-zip": "^7.4.0",
|
||||||
"@electron-forge/maker-flatpak": "^7.4.0",
|
"@electron/rebuild": "^3.6.0",
|
||||||
"@electron-forge/maker-dmg": "^7.4.0",
|
"electron": "30.0.1"
|
||||||
"electron": "30.0.1",
|
|
||||||
"@electron/rebuild": "^3.6.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dombo": "^3.2.0",
|
"dombo": "^3.2.0",
|
||||||
@@ -31,7 +32,9 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"forge": {
|
"forge": {
|
||||||
"packagerConfig": {},
|
"packagerConfig": {
|
||||||
|
"icon": "images/icon"
|
||||||
|
},
|
||||||
"make_targets": {
|
"make_targets": {
|
||||||
"win32": [
|
"win32": [
|
||||||
"squirrel"
|
"squirrel"
|
||||||
@@ -49,7 +52,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@electron-forge/maker-squirrel",
|
"name": "@electron-forge/maker-squirrel",
|
||||||
"config": {
|
"config": {
|
||||||
"name": "refvroid"
|
"name": "refvroid",
|
||||||
|
"setupIcon": "images/icon.ico"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
304
preload.js
304
preload.js
@@ -41,6 +41,121 @@ let state = {
|
|||||||
isLocked: false
|
isLocked: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 撤销/重做历史记录
|
||||||
|
let history = {
|
||||||
|
undoStack: [],
|
||||||
|
redoStack: [],
|
||||||
|
maxSize: 50
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前状态到历史记录
|
||||||
|
function saveStateToHistory() {
|
||||||
|
// 创建状态快照(深拷贝)
|
||||||
|
const snapshot = {
|
||||||
|
elements: state.elements.map(el => ({
|
||||||
|
path: el.path,
|
||||||
|
type: el.type,
|
||||||
|
x: parseFloat(el.element.dataset.x) || 0,
|
||||||
|
y: parseFloat(el.element.dataset.y) || 0,
|
||||||
|
width: el.width,
|
||||||
|
height: el.height,
|
||||||
|
loopPairs: el.loopPairs,
|
||||||
|
activeLoopPair: el.activeLoopPair
|
||||||
|
})),
|
||||||
|
currentScale: state.currentScale,
|
||||||
|
translate: { ...state.translate }
|
||||||
|
}
|
||||||
|
|
||||||
|
history.undoStack.push(snapshot)
|
||||||
|
|
||||||
|
// 限制历史记录大小
|
||||||
|
if (history.undoStack.length > history.maxSize) {
|
||||||
|
history.undoStack.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空重做栈
|
||||||
|
history.redoStack = []
|
||||||
|
|
||||||
|
console.log('State saved to history, undo stack size:', history.undoStack.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 撤销
|
||||||
|
function undo() {
|
||||||
|
if (history.undoStack.length === 0) {
|
||||||
|
console.log('Nothing to undo')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前状态到重做栈
|
||||||
|
const currentSnapshot = {
|
||||||
|
elements: state.elements.map(el => ({
|
||||||
|
path: el.path,
|
||||||
|
type: el.type,
|
||||||
|
x: parseFloat(el.element.dataset.x) || 0,
|
||||||
|
y: parseFloat(el.element.dataset.y) || 0,
|
||||||
|
width: el.width,
|
||||||
|
height: el.height,
|
||||||
|
loopPairs: el.loopPairs,
|
||||||
|
activeLoopPair: el.activeLoopPair
|
||||||
|
})),
|
||||||
|
currentScale: state.currentScale,
|
||||||
|
translate: { ...state.translate }
|
||||||
|
}
|
||||||
|
history.redoStack.push(currentSnapshot)
|
||||||
|
|
||||||
|
// 恢复上一个状态
|
||||||
|
const snapshot = history.undoStack.pop()
|
||||||
|
restoreStateFromSnapshot(snapshot)
|
||||||
|
|
||||||
|
console.log('Undo performed, undo stack:', history.undoStack.length, 'redo stack:', history.redoStack.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重做
|
||||||
|
function redo() {
|
||||||
|
if (history.redoStack.length === 0) {
|
||||||
|
console.log('Nothing to redo')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前状态到撤销栈
|
||||||
|
const currentSnapshot = {
|
||||||
|
elements: state.elements.map(el => ({
|
||||||
|
path: el.path,
|
||||||
|
type: el.type,
|
||||||
|
x: parseFloat(el.element.dataset.x) || 0,
|
||||||
|
y: parseFloat(el.element.dataset.y) || 0,
|
||||||
|
width: el.width,
|
||||||
|
height: el.height,
|
||||||
|
loopPairs: el.loopPairs,
|
||||||
|
activeLoopPair: el.activeLoopPair
|
||||||
|
})),
|
||||||
|
currentScale: state.currentScale,
|
||||||
|
translate: { ...state.translate }
|
||||||
|
}
|
||||||
|
history.undoStack.push(currentSnapshot)
|
||||||
|
|
||||||
|
// 恢复重做状态
|
||||||
|
const snapshot = history.redoStack.pop()
|
||||||
|
restoreStateFromSnapshot(snapshot)
|
||||||
|
|
||||||
|
console.log('Redo performed, undo stack:', history.undoStack.length, 'redo stack:', history.redoStack.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从快照恢复状态
|
||||||
|
function restoreStateFromSnapshot(snapshot) {
|
||||||
|
// 清除当前所有元素
|
||||||
|
state.elements.forEach(el => el.element.remove())
|
||||||
|
state.elements = []
|
||||||
|
|
||||||
|
// 恢复缩放和平移
|
||||||
|
updateScaleAndTranslate(snapshot.currentScale, snapshot.translate)
|
||||||
|
|
||||||
|
// 恢复所有元素
|
||||||
|
snapshot.elements.forEach(elementData => {
|
||||||
|
addMediaWithPath(elementData.path, elementData.type, elementData)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let videoExample = {
|
let videoExample = {
|
||||||
loopPairs: [[0, 119], [44, 56]], // can derive A, B, C & color coding from index
|
loopPairs: [[0, 119], [44, 56]], // can derive A, B, C & color coding from index
|
||||||
activeLoopPair: 0
|
activeLoopPair: 0
|
||||||
@@ -205,6 +320,18 @@ document.addEventListener('keydown', evt => {
|
|||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
copySelected()
|
copySelected()
|
||||||
console.log('Ctrl+C was pressed')
|
console.log('Ctrl+C was pressed')
|
||||||
|
} else if (evt.key === 'z' && evt.ctrlKey && !evt.shiftKey) {
|
||||||
|
evt.preventDefault()
|
||||||
|
undo()
|
||||||
|
console.log('Ctrl+Z was pressed (Undo)')
|
||||||
|
} else if (evt.key === 'y' && evt.ctrlKey) {
|
||||||
|
evt.preventDefault()
|
||||||
|
redo()
|
||||||
|
console.log('Ctrl+Y was pressed (Redo)')
|
||||||
|
} else if (evt.key === 'z' && evt.ctrlKey && evt.shiftKey) {
|
||||||
|
evt.preventDefault()
|
||||||
|
redo()
|
||||||
|
console.log('Ctrl+Shift+Z was pressed (Redo)')
|
||||||
} else if (evt.key === 'Delete') {
|
} else if (evt.key === 'Delete') {
|
||||||
console.log('delete selected')
|
console.log('delete selected')
|
||||||
deleteSelected()
|
deleteSelected()
|
||||||
@@ -593,6 +720,11 @@ function addMediaWithPath(path, type = 'img', loadedState) {
|
|||||||
//adjustFontSize2(mediaElement, path, loadedState.width)
|
//adjustFontSize2(mediaElement, path, loadedState.width)
|
||||||
} else
|
} else
|
||||||
setTransformForElement(zIndex)
|
setTransformForElement(zIndex)
|
||||||
|
|
||||||
|
// 保存历史记录(仅在添加新元素时,不在恢复状态时)
|
||||||
|
if (isNewElement) {
|
||||||
|
saveStateToHistory()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function adjustFontSize2(mediaElement, text, maxWidth = window.innerWidth) {
|
function adjustFontSize2(mediaElement, text, maxWidth = window.innerWidth) {
|
||||||
temp = document.createElement('div')
|
temp = document.createElement('div')
|
||||||
@@ -631,16 +763,23 @@ document.addEventListener('drop', (event) => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
// Todo check if file is valid
|
// Todo check if file is valid
|
||||||
|
|
||||||
|
|
||||||
for (const f of event.dataTransfer.files) {
|
for (const f of event.dataTransfer.files) {
|
||||||
// Using the path attribute to get absolute file path
|
// Using the path attribute to get absolute file path
|
||||||
console.log('File Path of dragged files: ', f.path, state)
|
console.log('File Path of dragged files: ', f.path, state)
|
||||||
|
|
||||||
if (f.path.endsWith('.mp4')) {
|
if (f.path.endsWith('.mp4')) {
|
||||||
addMediaWithPath(f.path, 'video')
|
addMediaWithPath(f.path, 'video')
|
||||||
} else
|
} else {
|
||||||
addMediaWithPath(f.path)
|
// 发送文件路径到主进程,让主进程转换为 base64
|
||||||
|
ipcRenderer.send('convert-file-to-dataurl', f.path)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 接收主进程转换后的 dataURL
|
||||||
|
ipcRenderer.on('file-converted-to-dataurl', (event, dataURL) => {
|
||||||
|
console.log('Received dataURL from main process')
|
||||||
|
addMediaWithPath(dataURL, 'dataURL')
|
||||||
});
|
});
|
||||||
function init() {
|
function init() {
|
||||||
document.documentElement.addEventListener('mousedown', (event) => {
|
document.documentElement.addEventListener('mousedown', (event) => {
|
||||||
@@ -739,7 +878,13 @@ contextBridge.exposeInMainWorld('myAPI', {
|
|||||||
|
|
||||||
interact('.draggable')
|
interact('.draggable')
|
||||||
.draggable({
|
.draggable({
|
||||||
listeners: { move: dragMoveListener },
|
listeners: {
|
||||||
|
move: dragMoveListener,
|
||||||
|
end: function(event) {
|
||||||
|
// 拖动结束时保存历史记录
|
||||||
|
saveStateToHistory()
|
||||||
|
}
|
||||||
|
},
|
||||||
inertia: false,
|
inertia: false,
|
||||||
}).on('tap', function (event) {
|
}).on('tap', function (event) {
|
||||||
var target = event.target
|
var target = event.target
|
||||||
@@ -775,6 +920,10 @@ interact('.selectedItem').resizable({
|
|||||||
}
|
}
|
||||||
setTransformForElement(target.dataset.zIndex, event.deltaRect.left, event.deltaRect.top, event.rect.width, event.rect.height)
|
setTransformForElement(target.dataset.zIndex, event.deltaRect.left, event.deltaRect.top, event.rect.width, event.rect.height)
|
||||||
forceRedraw()
|
forceRedraw()
|
||||||
|
},
|
||||||
|
end(event) {
|
||||||
|
// 调整大小结束时保存历史记录
|
||||||
|
saveStateToHistory()
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
modifiers: [
|
modifiers: [
|
||||||
@@ -790,7 +939,13 @@ interact('.selectedItem').resizable({
|
|||||||
],
|
],
|
||||||
inertia: true
|
inertia: true
|
||||||
}).draggable({
|
}).draggable({
|
||||||
listeners: { move: dragMoveListener },
|
listeners: {
|
||||||
|
move: dragMoveListener,
|
||||||
|
end: function(event) {
|
||||||
|
// 拖动结束时保存历史记录
|
||||||
|
saveStateToHistory()
|
||||||
|
}
|
||||||
|
},
|
||||||
inertia: false
|
inertia: false
|
||||||
}).on('tap', function (event) {
|
}).on('tap', function (event) {
|
||||||
|
|
||||||
@@ -837,6 +992,26 @@ function copySelected() {
|
|||||||
function copyImageToClipboard(selectedElement) {
|
function copyImageToClipboard(selectedElement) {
|
||||||
try {
|
try {
|
||||||
const img = selectedElement.element
|
const img = selectedElement.element
|
||||||
|
const imgSrc = img.src || selectedElement.path
|
||||||
|
|
||||||
|
console.log('Copying image - type:', selectedElement.type)
|
||||||
|
console.log('Image src:', imgSrc.substring(0, 100))
|
||||||
|
|
||||||
|
// 检查是否是 GIF(通过 dataURL 或文件扩展名)
|
||||||
|
const isGif = imgSrc.startsWith('data:image/gif') ||
|
||||||
|
imgSrc.toLowerCase().endsWith('.gif') ||
|
||||||
|
(selectedElement.path && selectedElement.path.toLowerCase().endsWith('.gif'))
|
||||||
|
|
||||||
|
if (isGif && imgSrc.startsWith('data:')) {
|
||||||
|
// 如果是 GIF 的 dataURL,直接使用原始数据
|
||||||
|
console.log('Copying GIF with original data (preserving animation)')
|
||||||
|
ipcRenderer.send('copy-image', imgSrc)
|
||||||
|
} else if (selectedElement.type === 'dataURL' && imgSrc.startsWith('data:')) {
|
||||||
|
// 如果是其他格式的 dataURL,也直接使用
|
||||||
|
console.log('Copying image with original dataURL')
|
||||||
|
ipcRenderer.send('copy-image', imgSrc)
|
||||||
|
} else {
|
||||||
|
// 对于非 dataURL 的图片,使用 Canvas 转换
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = img.naturalWidth || img.width
|
canvas.width = img.naturalWidth || img.width
|
||||||
canvas.height = img.naturalHeight || img.height
|
canvas.height = img.naturalHeight || img.height
|
||||||
@@ -844,13 +1019,14 @@ function copyImageToClipboard(selectedElement) {
|
|||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
ctx.drawImage(img, 0, 0)
|
ctx.drawImage(img, 0, 0)
|
||||||
|
|
||||||
// 转换为 PNG 格式的 dataURL(更好的兼容性)
|
// 转换为 PNG 格式的 dataURL
|
||||||
const pngDataURL = canvas.toDataURL('image/png')
|
const pngDataURL = canvas.toDataURL('image/png')
|
||||||
|
|
||||||
console.log('Copying image - type:', selectedElement.type, 'size:', canvas.width, 'x', canvas.height)
|
console.log('Copying image converted to PNG, size:', canvas.width, 'x', canvas.height)
|
||||||
ipcRenderer.send('copy-image', pngDataURL)
|
ipcRenderer.send('copy-image', pngDataURL)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error converting image to canvas:', err)
|
console.error('Error copying image:', err)
|
||||||
// 降级方案:直接使用原始路径
|
// 降级方案:直接使用原始路径
|
||||||
const imgSrc = selectedElement.element.src || selectedElement.path
|
const imgSrc = selectedElement.element.src || selectedElement.path
|
||||||
ipcRenderer.send('copy-image', imgSrc)
|
ipcRenderer.send('copy-image', imgSrc)
|
||||||
@@ -871,12 +1047,29 @@ async function copyMultipleImages(selectedElements) {
|
|||||||
|
|
||||||
console.log('Copying', imageElements.length, 'images')
|
console.log('Copying', imageElements.length, 'images')
|
||||||
|
|
||||||
// 收集所有图片的 Blob
|
// 收集所有图片的原始数据(保持 GIF 动画)
|
||||||
const imageBlobs = []
|
const imageDataURLs = []
|
||||||
|
|
||||||
for (const el of imageElements) {
|
for (const el of imageElements) {
|
||||||
try {
|
try {
|
||||||
const img = el.element
|
const img = el.element
|
||||||
|
const imgSrc = img.src || el.path
|
||||||
|
|
||||||
|
// 检查是否是 GIF
|
||||||
|
const isGif = imgSrc.startsWith('data:image/gif') ||
|
||||||
|
imgSrc.toLowerCase().endsWith('.gif') ||
|
||||||
|
(el.path && el.path.toLowerCase().endsWith('.gif'))
|
||||||
|
|
||||||
|
if (isGif && imgSrc.startsWith('data:')) {
|
||||||
|
// GIF dataURL - 直接使用原始数据
|
||||||
|
console.log(' Preserving GIF animation for element')
|
||||||
|
imageDataURLs.push(imgSrc)
|
||||||
|
} else if (el.type === 'dataURL' && imgSrc.startsWith('data:')) {
|
||||||
|
// 其他 dataURL - 直接使用
|
||||||
|
console.log(' Using original dataURL for element')
|
||||||
|
imageDataURLs.push(imgSrc)
|
||||||
|
} else {
|
||||||
|
// 其他格式 - 转换为 PNG
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = img.naturalWidth || img.width
|
canvas.width = img.naturalWidth || img.width
|
||||||
canvas.height = img.naturalHeight || img.height
|
canvas.height = img.naturalHeight || img.height
|
||||||
@@ -884,68 +1077,24 @@ async function copyMultipleImages(selectedElements) {
|
|||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
ctx.drawImage(img, 0, 0)
|
ctx.drawImage(img, 0, 0)
|
||||||
|
|
||||||
// 转换为 Blob
|
const pngDataURL = canvas.toDataURL('image/png')
|
||||||
const blob = await new Promise(resolve => {
|
console.log(' Converted to PNG for element')
|
||||||
canvas.toBlob(resolve, 'image/png')
|
imageDataURLs.push(pngDataURL)
|
||||||
})
|
|
||||||
|
|
||||||
if (blob) {
|
|
||||||
imageBlobs.push(blob)
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error converting image:', err)
|
console.error('Error processing image:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageBlobs.length === 0) {
|
if (imageDataURLs.length === 0) {
|
||||||
console.log('No valid images to copy')
|
console.log('No valid images to copy')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 Clipboard API 复制多张图片
|
// 发送到主进程处理
|
||||||
try {
|
console.log('Sending', imageDataURLs.length, 'images to main process')
|
||||||
const clipboardItems = imageBlobs.map(blob =>
|
|
||||||
new ClipboardItem({ 'image/png': blob })
|
|
||||||
)
|
|
||||||
|
|
||||||
// 注意:标准 Clipboard API 一次只能写入一个 ClipboardItem
|
|
||||||
// 所以我们需要将多张图片合并或者只复制第一张
|
|
||||||
// 这里我们尝试写入所有图片,但浏览器可能只保留最后一张
|
|
||||||
for (const item of clipboardItems) {
|
|
||||||
await navigator.clipboard.write([item])
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Copied', imageBlobs.length, 'images using Clipboard API')
|
|
||||||
|
|
||||||
// 降级方案:发送到主进程
|
|
||||||
const imageDataURLs = []
|
|
||||||
for (const el of imageElements) {
|
|
||||||
const img = el.element
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = img.naturalWidth || img.width
|
|
||||||
canvas.height = img.naturalHeight || img.height
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
ctx.drawImage(img, 0, 0)
|
|
||||||
imageDataURLs.push(canvas.toDataURL('image/png'))
|
|
||||||
}
|
|
||||||
ipcRenderer.send('copy-multiple-images', imageDataURLs)
|
ipcRenderer.send('copy-multiple-images', imageDataURLs)
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Clipboard API failed, using fallback:', err)
|
|
||||||
// 降级方案:发送到主进程
|
|
||||||
const imageDataURLs = []
|
|
||||||
for (const el of imageElements) {
|
|
||||||
const img = el.element
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = img.naturalWidth || img.width
|
|
||||||
canvas.height = img.naturalHeight || img.height
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
ctx.drawImage(img, 0, 0)
|
|
||||||
imageDataURLs.push(canvas.toDataURL('image/png'))
|
|
||||||
}
|
|
||||||
ipcRenderer.send('copy-multiple-images', imageDataURLs)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error copying multiple images:', err)
|
console.error('Error copying multiple images:', err)
|
||||||
ipcRenderer.send('copy-text', '复制多张图片失败: ' + err.message)
|
ipcRenderer.send('copy-text', '复制多张图片失败: ' + err.message)
|
||||||
@@ -955,11 +1104,36 @@ async function copyMultipleImages(selectedElements) {
|
|||||||
function deleteSelected() {
|
function deleteSelected() {
|
||||||
if (state.elements.length == 0) return
|
if (state.elements.length == 0) return
|
||||||
|
|
||||||
if (!state.elements[state.elements.length - 1].element.classList.contains('selectedItem')) return
|
// 收集所有选中的元素
|
||||||
|
const selectedElements = state.elements.filter(el =>
|
||||||
|
el.element.classList.contains('selectedItem')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedElements.length === 0) return
|
||||||
|
|
||||||
|
console.log('Deleting', selectedElements.length, 'selected element(s)')
|
||||||
|
|
||||||
|
// 保存历史记录
|
||||||
|
saveStateToHistory()
|
||||||
|
|
||||||
|
// 删除所有选中的元素
|
||||||
|
selectedElements.forEach(elementObj => {
|
||||||
|
// 从 DOM 中移除
|
||||||
|
elementObj.element.remove()
|
||||||
|
|
||||||
|
// 从 state.elements 数组中移除
|
||||||
|
const index = state.elements.indexOf(elementObj)
|
||||||
|
if (index > -1) {
|
||||||
|
state.elements.splice(index, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重新设置所有剩余元素的 zIndex
|
||||||
|
state.elements.forEach((elementObj, index) => {
|
||||||
|
elementObj.element.style.zIndex = index
|
||||||
|
elementObj.element.dataset.zIndex = index
|
||||||
|
})
|
||||||
|
|
||||||
state.elements[state.elements.length - 1].element.remove();
|
|
||||||
//delete state.elements[state.elements.length - 1].element
|
|
||||||
state.elements.pop();
|
|
||||||
clearAllSelected()
|
clearAllSelected()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user