Appearance
剪贴板导入优化功能
概述
Eagle2Ae AE 扩展 v2.4.0 对剪贴板导入功能进行了深度优化,新增了剪贴板图片自动检测、临时文件智能重命名、优化的确认对话框等功能。这些改进使得从剪贴板导入图片变得更加便捷和智能。
核心特性
剪贴板图片自动检测
- 自动检测剪贴板中的图片内容
- 支持多种图片格式(PNG、JPG、GIF、BMP、WebP等)
- 提供实时检测和导入提示
临时文件智能重命名
- 自动识别剪贴板图片的真实文件名
- 智能重命名临时文件,避免使用通用名称
- 保持文件扩展名正确性
优化的确认对话框
- 简洁明了的文件信息展示
- 实时显示当前导入设置
- 支持快速导入操作
增强的兼容性
- 支持现代剪贴板API和传统API
- 兼容不同操作系统的剪贴板行为
- 提供降级处理机制
使用指南
基本剪贴板导入
复制图片到剪贴板
- 在网页浏览器中右键复制图片
- 在图片查看器中使用复制功能
- 在截图工具中截取并复制图片
- 在微信、QQ等聊天软件中复制图片
触发剪贴板导入
- 确保Eagle2Ae扩展面板处于激活状态
- 按下快捷键
Ctrl+V(Windows) 或Cmd+V(macOS) - 系统将自动检测剪贴板内容并显示导入对话框
确认导入
- 在导入对话框中确认文件信息
- 检查导入设置是否正确
- 点击"导入文件"按钮开始导入
高级功能
智能文件名识别
系统会尝试从剪贴板中获取原始文件名:
- 网页图片:尝试从图片URL中提取文件名
- 本地文件:使用实际文件名
- 截图工具:使用时间戳命名
文件类型检测
支持以下图片格式的自动检测:
- PNG (.png)
- JPEG/JPG (.jpg, .jpeg)
- GIF (.gif)
- BMP (.bmp)
- WebP (.webp)
- TIFF (.tiff, .tif)
- SVG (.svg)
临时文件处理
对于无法获取原始文件名的临时文件:
- 生成基于时间戳的文件名
- 保持正确的文件扩展名
- 避免与现有文件名冲突
技术实现
剪贴板监听实现
javascript
/**
* 设置剪贴板监听
*/
setupClipboardListener() {
try {
// 监听键盘事件,检测Ctrl+V/Cmd+V
document.addEventListener('keydown', (e) => {
// 检测Ctrl+V (Windows) 或 Cmd+V (Mac)
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
// 延迟一点执行,确保剪贴板内容已更新
setTimeout(() => {
this.handleClipboardPaste(e);
}, 50);
}
});
// 也监听paste事件作为备用
document.addEventListener('paste', (e) => {
this.handleClipboardPaste(e);
});
this.log('📋 剪贴板监听器已设置', 'debug');
} catch (error) {
this.log(`设置剪贴板监听失败: ${error.message}`, 'error');
}
}剪贴板内容处理
javascript
/**
* 处理剪贴板粘贴事件
* @param {Event} event - 粘贴事件对象
*/
async handleClipboardPaste(event) {
try {
// 防止在输入框中触发
if (event.target && (
event.target.tagName === 'INPUT' ||
event.target.tagName === 'TEXTAREA' ||
event.target.contentEditable === 'true'
)) {
return;
}
this.log('🔍 检测到剪贴板粘贴操作', 'debug');
let clipboardData = null;
// 尝试从事件获取剪贴板数据
if (event.clipboardData) {
clipboardData = event.clipboardData;
} else {
// 尝试使用现代剪贴板API
try {
const clipboardItems = await navigator.clipboard.read();
if (clipboardItems && clipboardItems.length > 0) {
// 构造类似clipboardData的对象
clipboardData = {
files: [],
types: [],
getData: () => ''
};
// 首先尝试获取文本信息,可能包含文件名
let possibleFileName = null;
for (const item of clipboardItems) {
if (item.types.includes('text/plain')) {
try {
const text = await item.getType('text/plain');
const textContent = await text.text();
// 检查文本是否像文件路径
const filePathMatch = textContent.match(/([^\\/]+\\.(jpg|jpeg|png|gif|bmp|webp|tiff|svg))$/i);
if (filePathMatch) {
possibleFileName = filePathMatch[1];
}
} catch (e) {
// 忽略文本获取错误
}
}
}
for (const item of clipboardItems) {
for (const type of item.types) {
clipboardData.types.push(type);
if (type.startsWith('image/')) {
const blob = await item.getType(type);
const ext = type.split('/')[1] === 'jpeg' ? 'jpg' : type.split('/')[1];
// 智能文件名选择
let fileName;
if (possibleFileName && this.isValidImageFileName(possibleFileName)) {
// 使用检测到的原始文件名
fileName = possibleFileName;
} else {
// 使用通用名称,将被标记为临时文件
fileName = `clipboard_image.${ext}`;
}
const file = new File([blob], fileName, { type });
clipboardData.files.push(file);
}
}
}
}
} catch (clipboardError) {
this.log(`无法访问剪贴板API: ${clipboardError.message}`, 'debug');
}
}
if (!clipboardData) {
this.log('无法获取剪贴板数据', 'debug');
return;
}
// 检测剪贴板内容
const clipboardContent = await this.detectClipboardContent(clipboardData);
if (clipboardContent && clipboardContent.files.length > 0) {
this.log(`检测到剪贴板中有 ${clipboardContent.files.length} 个可导入文件`, 'info');
// 预处理文件名称,在显示对话框时就显示最终名称
const processedFiles = clipboardContent.files.map(file => {
if (file.isTemporary && !file.hasOriginalName) {
// 只有临时文件且没有原始名称时才重命名
const ext = this.getFileExtension(file.name);
const newName = this.generateTimestampFilename(ext);
return {
...file,
displayName: newName, // 用于显示的名称
originalName: file.name, // 保存原始名称
name: newName, // 更新实际名称
isTemporary: true,
wasRenamed: true // 标记已重命名
};
} else if (file.hasOriginalName) {
// 有原始名称的文件,保持原名
return {
...file,
displayName: file.name,
hasOriginalName: true
};
}
return {
...file,
displayName: file.name
};
});
this.showClipboardConfirmDialog({ ...clipboardContent, files: processedFiles });
} else {
this.log('剪贴板中没有可导入的内容', 'debug');
}
} catch (error) {
this.log(`处理剪贴板粘贴失败: ${error.message}`, 'error');
}
}智能文件名重命名
javascript
/**
* 智能生成时间戳文件名
* @param {string} originalExt - 原始文件扩展名
* @returns {string} 生成的文件名
*/
generateTimestampFilename(originalExt) {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hour = String(now.getHours()).padStart(2, '0');
const minute = String(now.getMinutes()).padStart(2, '0');
const second = String(now.getSeconds()).padStart(2, '0');
return `screenshot_${year}${month}${day}_${hour}${minute}${second}${originalExt}`;
}
/**
* 验证是否为有效的图片文件名
* @param {string} fileName - 文件名
* @returns {boolean} 是否为有效文件名
*/
isValidImageFileName(fileName) {
if (!fileName || typeof fileName !== 'string') return false;
// 检查是否有有效的图片扩展名
const imageExtensions = /\.(jpg|jpeg|png|gif|bmp|webp|tiff|svg)$/i;
if (!imageExtensions.test(fileName)) return false;
// 检查文件名是否合理(不是临时文件名)
const tempPatterns = [
/^clipboard_image\./i,
/^image\./i,
/^screenshot\./i,
/^capture\./i,
/^untitled\./i,
/^temp\./i,
/^tmp\./i
];
// 如果匹配任何临时文件模式,则认为不是有效的原始文件名
for (const pattern of tempPatterns) {
if (pattern.test(fileName)) {
return false;
}
}
// 检查文件名长度和字符
if (fileName.length < 5 || fileName.length > 255) return false;
// 检查是否包含非法字符
const illegalChars = /[<>:"|?*\x00-\x1f]/;
if (illegalChars.test(fileName)) return false;
return true;
}临时文件检测
javascript
/**
* 增强的临时文件检测(专门用于剪贴板导入)
* @param {string} fileName - 文件名
* @returns {boolean} 是否为临时文件
*/
isTemporaryFileEnhanced(fileName) {
if (!fileName) return false;
// 1. 检查是否为通用的剪贴板文件名
const genericNames = [
/^clipboard_image\.(png|jpg|jpeg|gif|bmp|webp)$/i,
/^image\.(png|jpg|jpeg|gif|bmp|webp)$/i,
/^screenshot\.(png|jpg|jpeg|gif|bmp|webp)$/i,
/^capture\.(png|jpg|jpeg|gif|bmp|webp)$/i,
/^untitled\.(png|jpg|jpeg|gif|bmp|webp)$/i
];
for (const pattern of genericNames) {
if (pattern.test(fileName)) {
return true;
}
}
// 2. 使用原有的临时文件检测逻辑
return this.isTemporaryFile(fileName);
}
/**
* 检查文件是否为临时文件
* @param {string} filePath - 文件路径
* @returns {boolean} 是否为临时文件
*/
isTemporaryFile(filePath) {
if (!filePath) return false;
const tempKeywords = [
'temp', 'tmp', 'temporary',
'screenshot', 'screen shot', 'screen_shot', 'capture',
'snip', 'clip', 'paste', 'clipboard',
'appdata\\local\\temp', 'appdata/local/temp',
'/tmp/', '/var/tmp/', '/private/tmp/',
'c:\\windows\\temp', 'c:/windows/temp',
'\\temp\\', '/temp/',
'snipping tool', 'snipaste', 'lightshot'
];
const pathLower = filePath.toLowerCase();
return tempKeywords.some(keyword => pathLower.includes(keyword.toLowerCase()));
}导入确认对话框
功能特点
简洁的文件信息展示
- 清晰显示文件名、大小、类型
- 使用图标区分不同文件类型
- 提供文件数量统计
实时设置预览
- 显示当前导入模式
- 展示时间轴放置行为
- 提供设置修改入口
快速操作
- 一键导入所有文件
- 支持取消导入操作
- 自动超时关闭
界面设计
html
<!-- 剪贴板导入确认对话框 -->
<div class="eagle-confirm-dialog">
<div class="eagle-confirm-content">
<div class="eagle-confirm-header">
<h3>剪贴板导入确认</h3>
</div>
<div class="eagle-confirm-body">
<p>检测到剪贴板中有 2 个可导入文件</p>
<div class="file-list">
<div class="file-item-simple">
<span class="file-icon">🖼️</span>
<span class="file-name" title="screenshot_20250915_143022.png">screenshot_20250915_143022.png</span>
<span class="file-size">1.2 MB</span>
<span class="file-type">image/png</span>
</div>
<div class="file-item-simple">
<span class="file-icon">🎬</span>
<span class="file-name" title="capture_20250915_143045.jpg">capture_20250915_143045.jpg</span>
<span class="file-size">856 KB</span>
<span class="file-type">image/jpeg</span>
</div>
</div>
<div class="import-settings-dark">
<div class="setting-item"><span class="setting-label">导入模式:</span><span class="setting-value">项目旁复制</span></div>
<div class="setting-item"><span class="setting-label">导入行为:</span><span class="setting-value">当前时间</span></div>
</div>
</div>
<div class="eagle-confirm-actions-flex">
<button class="btn-outline-primary" id="clipboard-confirm-yes">导入文件</button>
<button class="btn-outline-secondary" id="clipboard-confirm-no">取消</button>
</div>
</div>
</div>最佳实践
使用建议
高效操作流程
- 在截图工具中截取图片后直接粘贴到扩展面板
- 结合快捷键使用,提高工作效率
- 合理利用自动重命名功能
文件管理
- 定期清理不需要的临时文件
- 建立合理的文件命名规范
- 使用有意义的文件名便于后续查找
质量控制
- 检查剪贴板图片的质量和分辨率
- 避免导入过大尺寸的图片
- 注意图片的版权和使用权限
性能优化
剪贴板处理优化
javascript// 使用防抖处理避免频繁检查 if (this.clipboardCheckTimeout) { clearTimeout(this.clipboardCheckTimeout); } this.clipboardCheckTimeout = setTimeout(() => { this.performClipboardCheck(); }, 100); // 100ms 防抖延迟文件处理优化
javascript// 对于大文件,提供进度反馈 const fileSize = file.size; if (fileSize > 10 * 1024 * 1024) { // 10MB this.showProgressIndicator('处理大文件中...'); }内存管理
javascript// 及时释放Blob URL const url = URL.createObjectURL(blob); // 使用完后及时释放 setTimeout(() => URL.revokeObjectURL(url), 1000);
故障排除
常见问题
剪贴板导入无响应
- 症状:按下Ctrl+V后无任何反应
- 解决:检查扩展面板是否处于激活状态,重启AE
文件名重命名失败
- 症状:临时文件名未被正确重命名
- 解决:检查文件名验证逻辑,确保时间戳生成正确
图片格式不支持
- 症状:某些格式的图片无法导入
- 解决:检查文件类型检测逻辑,添加缺失的格式支持
调试技巧
启用详细日志
javascript// 在控制台中设置日志级别 localStorage.setItem('debugLogLevel', '0'); // 0=debug, 1=info, 2=warning, 3=error监控剪贴板事件
javascript// 在handleClipboardPaste方法中添加详细日志 this.log(`📋 剪贴板事件: ${event.type}`, 'debug'); this.log(`📋 剪贴板数据类型: ${Array.from(clipboardData.types).join(', ')}`, 'debug');性能监控
javascript// 记录处理时间 const startTime = Date.now(); // ... 处理逻辑 ... const endTime = Date.now(); this.log(`⏱️ 剪贴板处理耗时: ${endTime - startTime}ms`, 'debug');
兼容性说明
操作系统兼容性
- Windows 10/11: 完全支持
- macOS 10.14+: 完全支持
- Linux: 部分支持(依赖浏览器兼容性)
浏览器兼容性
- Chrome 86+: 完全支持现代剪贴板API
- Firefox 87+: 支持基础功能
- Safari 13.1+: 支持基础功能
- Edge 86+: 完全支持
AE版本兼容性
- After Effects CC 2018+: 完全兼容
- After Effects 2023+: 推荐使用