Appearance
剪贴板导入优化功能
概述
剪贴板导入优化功能(Optimized Clipboard Import)是 Eagle2Ae AE 扩展 v2.4.0 引入的重要功能模块,它显著提升了从剪贴板导入图片的效率和用户体验。该功能新增了剪贴板图片自动检测、临时文件智能重命名、优化的确认对话框等多项功能,让剪贴板导入变得更加智能和便捷。
核心特性
剪贴板图片自动检测
- 自动检测剪贴板中的图片内容
- 支持多种图片格式(PNG、JPG、GIF、BMP、WebP等)
- 提供实时检测和导入提示
临时文件智能重命名
- 自动识别剪贴板图片的真实文件名
- 智能重命名临时文件,避免使用通用名称
- 保持文件扩展名正确性
优化的确认对话框
- 简洁明了的文件信息展示
- 实时显示当前导入设置
- 支持快速导入操作
增强的兼容性
- 支持现代剪贴板API和传统API
- 兼容不同操作系统的剪贴板行为
- 提供降级处理机制
技术实现
核心类结构
javascript
/**
* 剪贴板导入优化功能
* 负责剪贴板图片检测、智能重命名、确认对话框等优化功能
*/
class OptimizedClipboardImport {
/**
* 构造函数
* @param {Object} aeExtension - AE扩展实例
*/
constructor(aeExtension) {
this.aeExtension = aeExtension;
this.csInterface = aeExtension.csInterface;
this.settingsManager = aeExtension.settingsManager;
this.logManager = aeExtension.logManager;
this.projectStatusChecker = aeExtension.projectStatusChecker;
this.soundPlayer = aeExtension.soundPlayer;
// 初始化状态
this.clipboardCheckTimeout = null;
this.lastClipboardContent = null;
this.clipboardDetectionCount = 0;
// 绑定方法上下文
this.setupClipboardListener = this.setupClipboardListener.bind(this);
this.handleClipboardPaste = this.handleClipboardPaste.bind(this);
this.detectClipboardContent = this.detectClipboardContent.bind(this);
this.showClipboardConfirmDialog = this.showClipboardConfirmDialog.bind(this);
this.handleClipboardImport = this.handleClipboardImport.bind(this);
this.log('📋 剪贴板导入优化功能已初始化', 'debug');
}
}剪贴板监听实现
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');
}
}
/**
* 处理剪贴板粘贴事件
* @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 {
// 尝试使用现代的 Clipboard 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 {Object} clipboardData - 剪贴板数据
* @returns {Promise<Object|null>} 检测结果或null
*/
async detectClipboardContent(clipboardData) {
try {
const result = {
files: [],
hasImages: false,
hasFilePaths: false
};
// 检查文件
if (clipboardData.files && clipboardData.files.length > 0) {
const files = Array.from(clipboardData.files);
for (const file of files) {
if (this.isImportableFile(file)) {
const fileName = file.path || file.webkitRelativePath || file.name;
// 改进的临时文件检测逻辑
const isTemp = this.isTemporaryFileEnhanced(fileName);
result.files.push({
name: file.name,
path: file.path || file.webkitRelativePath || file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified || Date.now(),
isClipboardImport: true,
isTemporary: isTemp,
hasOriginalName: !isTemp, // 如果不是临时文件,说明有原始名称
file: file // 保存原始文件对象
});
result.hasImages = true;
}
}
}
// 检查文本内容(可能包含文件路径)
if (clipboardData.getData) {
const textData = clipboardData.getData('text/plain') || '';
if (textData.trim()) {
const filePaths = this.extractFilePathsFromText(textData);
if (filePaths.length > 0) {
result.hasFilePaths = true;
// 这里可以进一步处理文件路径,但需要文件系统访问权限
this.log(`📋 检测到 ${filePaths.length} 个文件路径`, 'debug');
}
}
}
return result.files.length > 0 ? result : null;
} catch (error) {
this.log(`检测剪贴板内容失败: ${error.message}`, 'error');
return null;
}
}
/**
* 检查文件是否可导入
* @param {File} file - 文件对象
* @returns {boolean} 是否可导入
*/
isImportableFile(file) {
if (!file || !file.type) return false;
const importableTypes = [
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp',
'image/tiff', 'image/webp', 'image/svg+xml',
'video/mp4', 'video/mov', 'video/avi', 'video/mkv', 'video/webm',
'audio/mp3', 'audio/wav', 'audio/aac', 'audio/flac', 'audio/ogg'
];
return importableTypes.some(type => file.type.startsWith(type.split('/')[0]));
}
/**
* 增强的临时文件检测(专门用于剪贴板导入)
* @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);
}剪贴板确认对话框实现
javascript
/**
* 显示剪贴板确认对话框
* @param {Object} clipboardContent - 剪贴板内容
*/
showClipboardConfirmDialog(clipboardContent) {
try {
const files = clipboardContent.files;
const settings = this.settingsManager.getSettings();
// 创建确认对话框
const dialog = document.createElement('div');
dialog.className = 'eagle-confirm-dialog';
// 构建文件信息 - 简化为一行显示
const fileInfoHtml = files.map((file, index) => {
const sizeText = file.size ? this.formatFileSize(file.size) : '未知大小';
const typeIcon = this.getFileIcon(file);
const displayName = file.displayName || file.name;
const fileType = file.type || '未知类型';
return `
<div class="file-item-simple" data-file-index="${index}">
<span class="file-icon">${typeIcon}</span>
<span class="file-name" title="${displayName}">${displayName}</span>
<span class="file-size">${sizeText}</span>
<span class="file-type">${fileType}</span>
</div>
`;
}).join('');
// 构建导入设置信息 - 简化显示
const importModeText = {
'direct': '直接导入',
'project_adjacent': '项目旁复制',
'custom_folder': '自定义文件夹'
}[settings.mode] || settings.mode;
// 获取当前设置
const currentSettings = this.settingsManager.getSettings();
// 根据是否自动添加到合成来确定导入行为
let importBehavior;
if (currentSettings.addToComposition) {
// 如果自动添加到合成,显示时间轴放置位置
const timelinePlacement = {
'current_time': '当前时间',
'timeline_start': '时间轴开始'
}[currentSettings.timelineOptions?.placement] || '当前时间';
importBehavior = timelinePlacement;
} else {
// 如果不自动添加到合成,显示"不导入合成"
importBehavior = (window.i18n?.getText('common.doNotImportToComp') || '不导入合成');
}
let importMode = importModeText;
// 检查是否是序列帧或文件夹,并根据情况调整导入行为显示
// 只有当用户没有明确设置导入行为时,才显示特殊的序列帧/文件夹导入提示
if (hasSequences && settings.mode === 'direct') { // 假设直接导入模式下,序列帧导入是特殊行为
importBehavior = '序列帧导入';
} else if (folderCount > 0 && settings.mode === 'direct') { // 假设直接导入模式下,文件夹导入是特殊行为
importBehavior = '文件夹导入';
}
dialog.innerHTML = `
<div class="eagle-confirm-content">
<div class="eagle-confirm-header">
<h3>剪贴板导入确认</h3>
</div>
<div class="eagle-confirm-body">
<p>检测到剪贴板中有 ${files.length} 个可导入文件</p>
<div class="file-list">
${fileInfoHtml}
</div>
<div class="import-settings-dark">
<div class="setting-item"><span class="setting-label">导入模式:</span><span class="setting-value">${importModeText}</span></div>
<div class="setting-item"><span class="setting-label">导入行为:</span><span class="setting-value">${importBehavior}</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>
`;
document.body.appendChild(dialog);
// 绑定事件
document.getElementById('clipboard-confirm-yes').onclick = async () => {
dialog.remove();
this.log('用户确认导入剪贴板内容', 'info');
await this.handleClipboardImport(files);
};
document.getElementById('clipboard-confirm-no').onclick = () => {
dialog.remove();
this.log('用户取消剪贴板导入', 'info');
this.showDropMessage('已取消导入', 'info');
};
// 点击对话框外部关闭
dialog.addEventListener('click', (e) => {
if (e.target === dialog) {
dialog.remove();
this.log('剪贴板确认对话框被关闭', 'info');
}
});
// 15秒后自动关闭
setTimeout(() => {
if (dialog.parentNode) {
dialog.remove();
this.log('剪贴板确认对话框超时关闭', 'info');
}
}, 15000);
} catch (error) {
this.log(`显示剪贴板确认对话框失败: ${error.message}`, 'error');
}
}
/**
* 处理剪贴板导入
* @param {Array} files - 文件数组
*/
async handleClipboardImport(files) {
try {
// 不显示处理提示,直接开始导入
// 处理临时文件重命名并标记为已确认
const processedFiles = files.map(file => {
// 标记文件为已确认导入
const confirmedFile = {
...file,
confirmed: true
};
if (file.isTemporary && !file.customName && !file.wasRenamed) {
// 只有在用户没有自定义文件名且未重命名时才自动重命名
const ext = this.getFileExtension(file.name);
const newName = this.generateTimestampFilename(ext);
this.log(`临时文件重命名: ${file.name} -> ${newName}`, 'info');
return {
...confirmedFile,
name: newName,
originalName: file.originalName || file.name,
isTemporary: true
};
} else if (file.isTemporary && (file.customName || file.wasRenamed)) {
this.log(`保留文件名: ${file.name} (用户自定义: ${file.customName}, 已重命名: ${file.wasRenamed})`, 'info');
}
return confirmedFile;
});
// 构造消息对象,模拟文件导入消息格式
const message = {
type: 'import',
files: processedFiles,
source: 'clipboard_import',
timestamp: Date.now(),
isClipboardImport: true,
// 优化:跳过一些不必要的检查
skipValidation: true,
fastMode: true
};
// 调用现有的文件处理流程
const result = await this.handleImportFiles(message);
// 显示结果 - 改进判断逻辑
if (result && (result.success === true || result.importedCount > 0)) {
this.showDropMessage(`✅ 剪贴板导入成功 (${result.importedCount || 1} 个文件)`, 'success');
} else {
this.showDropMessage(`❌ 剪贴板导入失败: ${result?.error || '未知错误'}`, 'error');
}
} catch (error) {
this.log(`❌ 剪贴板导入失败: ${error.message}`, 'error');
this.showDropMessage(`❌ 剪贴板导入失败: ${error.message}`, 'error');
}
}API参考
核心方法
setupClipboardListener()
设置剪贴板监听器
javascript
/**
* 设置剪贴板监听器
*/
setupClipboardListener()handleClipboardPaste()
处理剪贴板粘贴事件
javascript
/**
* 处理剪贴板粘贴事件
* @param {Event} event - 粘贴事件对象
*/
async handleClipboardPaste(event)detectClipboardContent()
检测剪贴板内容
javascript
/**
* 检测剪贴板内容
* @param {Object} clipboardData - 剪贴板数据
* @returns {Promise<Object|null>} 检测结果或null
*/
async detectClipboardContent(clipboardData)isImportableFile()
检查文件是否可导入
javascript
/**
* 检查文件是否可导入
* @param {File} file - 文件对象
* @returns {boolean} 是否可导入
*/
isImportableFile(file)isTemporaryFileEnhanced()
增强的临时文件检测(专门用于剪贴板导入)
javascript
/**
* 增强的临时文件检测(专门用于剪贴板导入)
* @param {string} fileName - 文件名
* @returns {boolean} 是否为临时文件
*/
isTemporaryFileEnhanced(fileName)showClipboardConfirmDialog()
显示剪贴板确认对话框
javascript
/**
* 显示剪贴板确认对话框
* @param {Object} clipboardContent - 剪贴板内容
*/
showClipboardConfirmDialog(clipboardContent)handleClipboardImport()
处理剪贴板导入
javascript
/**
* 处理剪贴板导入
* @param {Array} files - 文件数组
*/
async handleClipboardImport(files)辅助方法
isValidImageFileName()
验证是否为有效的图片文件名
javascript
/**
* 验证是否为有效的图片文件名
* @param {string} fileName - 文件名
* @returns {boolean} 是否为有效的图片文件名
*/
isValidImageFileName(fileName)extractFilePathsFromText()
从文本中提取文件路径
javascript
/**
* 从文本中提取文件路径
* @param {string} text - 文本内容
* @returns {Array} 文件路径数组
*/
extractFilePathsFromText(text)generateTimestampFilename()
生成时间戳文件名
javascript
/**
* 生成时间戳文件名
* @param {string} originalExt - 原始扩展名
* @returns {string} 时间戳文件名
*/
generateTimestampFilename(originalExt)formatFileSize()
格式化文件大小
javascript
/**
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化的文件大小
*/
formatFileSize(bytes)getFileExtension()
获取文件扩展名
javascript
/**
* 获取文件扩展名
* @param {string} filename - 文件名
* @returns {string} 文件扩展名
*/
getFileExtension(filename)getFileNameWithoutExtension()
获取不含扩展名的文件名
javascript
/**
* 获取不含扩展名的文件名
* @param {string} filename - 文件名
* @returns {string} 不含扩展名的文件名
*/
getFileNameWithoutExtension(filename)getFileIcon()
获取文件图标
javascript
/**
* 获取文件图标
* @param {File} file - 文件对象
* @returns {string} 文件图标
*/
getFileIcon(file)getFileType()
获取文件类型
javascript
/**
* 获取文件类型
* @param {File} file - 文件对象
* @returns {string} 文件类型
*/
getFileType(file)isTemporaryFile()
检查是否为临时文件
javascript
/**
* 检查是否为临时文件
* @param {string} filePath - 文件路径
* @returns {boolean} 是否为临时文件
*/
isTemporaryFile(filePath)supportsWebkitDirectory()
检查是否支持webkitdirectory
javascript
/**
* 检查是否支持webkitdirectory
* @returns {boolean} 是否支持webkitdirectory
*/
supportsWebkitDirectory()useWebkitDirectoryPicker()
使用webkitdirectory API选择文件夹
javascript
/**
* 使用webkitdirectory API选择文件夹
*/
useWebkitDirectoryPicker()useCEPFolderPicker()
使用CEP ExtendScript方式选择文件夹
javascript
/**
* 使用CEP ExtendScript方式选择文件夹
*/
useCEPFolderPicker()fallbackToInputPrompt()
降级到输入提示方式
javascript
/**
* 降级到输入提示方式
* @param {string} currentPath - 当前路径
*/
fallbackToInputPrompt(currentPath)extractFolderPath()
从文件路径中提取文件夹路径
javascript
/**
* 从文件路径中提取文件夹路径
* @param {string} filePath - 文件路径
* @returns {string} 文件夹路径
*/
extractFolderPath(filePath)getDirectoryPath()
尝试获取目录的完整路径
javascript
/**
* 尝试获取目录的完整路径(File System Access API)
* @param {FileSystemDirectoryHandle} directoryHandle - 目录句柄
* @returns {Promise<string>} 目录路径
*/
async getDirectoryPath(directoryHandle)handleSelectedFolder()
处理选择的文件夹
javascript
/**
* 处理选择的文件夹(统一处理方法)
* @param {string} folderPath - 文件夹路径
*/
handleSelectedFolder(folderPath)showFolderPickerModal()
显示文件夹选择模态框
javascript
/**
* 显示文件夹选择模态框
*/
showFolderPickerModal()resetFolderPickerModal()
重置文件夹选择模态框状态
javascript
/**
* 重置文件夹选择模态框状态
*/
resetFolderPickerModal()setupFolderPickerEvents()
设置文件夹选择模态框事件监听器
javascript
/**
* 设置文件夹选择模态框事件监听器
*/
setupFolderPickerEvents()hideFolderPickerModal()
隐藏文件夹选择模态框
javascript
/**
* 隐藏文件夹选择模态框
*/
hideFolderPickerModal()tryModernWebFolderPicker()
尝试使用现代Web API选择文件夹
javascript
/**
* 尝试使用现代Web API选择文件夹
* @returns {boolean} 是否成功
*/
tryModernWebFolderPicker()useModernFolderPicker()
使用系统文件夹选择器
javascript
/**
* 使用系统文件夹选择器
*/
useModernFolderPicker()confirmFolderSelection()
确认文件夹选择
javascript
/**
* 确认文件夹选择
*/
confirmFolderSelection()setupDragDropEvents()
设置拖拽事件
javascript
/**
* 设置拖拽事件
*/
setupDragDropEvents()setupManualInputEvents()
设置手动输入事件
javascript
/**
* 设置手动输入事件
*/
setupManualInputEvents()loadRecentFoldersInModal()
在模态框中加载最近使用的文件夹
javascript
/**
* 在模态框中加载最近使用的文件夹(简化版 - 暂时不显示)
*/
loadRecentFoldersInModal()enableConfirmButton()
启用确认按钮
javascript
/**
* 启用确认按钮
*/
enableConfirmButton()toggleConfirmButton()
切换确认按钮状态
javascript
/**
* 切换确认按钮状态
* @param {boolean} enabled - 是否启用
*/
toggleConfirmButton(enabled)validateFolderPath()
验证文件夹路径
javascript
/**
* 验证文件夹路径
* @param {string} path - 文件夹路径
*/
validateFolderPath(path)updateRecentFoldersDropdown()
更新最近文件夹下拉列表
javascript
/**
* 更新最近文件夹下拉列表
*/
updateRecentFoldersDropdown()truncatePath()
截断路径显示
javascript
/**
* 截断路径显示
* @param {string} path - 路径
* @param {number} maxLength - 最大长度
* @returns {string} 截断后的路径
*/
truncatePath(path, maxLength)saveSettings()
保存设置
javascript
/**
* 保存设置
* @param {boolean} hidePanel - 是否隐藏面板
*/
saveSettings(hidePanel = true)resetSettings()
重置设置
javascript
/**
* 重置设置
*/
resetSettings()syncSettingsToEagle()
同步设置到Eagle插件
javascript
/**
* 同步设置到Eagle插件
* @param {Object} settings - 设置对象
*/
async syncSettingsToEagle(settings)syncPortToEagle()
智能端口同步
javascript
/**
* 智能端口同步 - 多端口尝试
* @param {number} oldPort - 旧端口
* @param {number} newPort - 新端口
* @returns {Promise<boolean>} 是否成功
*/
async syncPortToEagle(oldPort, newPort)updateEagleUrl()
更新Eagle URL
javascript
/**
* 更新Eagle URL
* @param {number} port - 端口号
*/
updateEagleUrl(port)updateEagleUrlWithDiscovery()
使用动态端口发现更新Eagle URL
javascript
/**
* 使用动态端口发现更新Eagle URL
*/
async updateEagleUrlWithDiscovery()startDemoLogs()
启动演示模式虚拟日志
javascript
/**
* 启动演示模式虚拟日志
* @param {number} port - 端口号
*/
startDemoLogs(port)startDemoActivityLogs()
启动演示活动日志
javascript
/**
* 启动演示活动日志
*/
startDemoActivityLogs()startEagleDemoLogs()
启动Eagle虚拟日志
javascript
/**
* 启动Eagle虚拟日志
*/
startEagleDemoLogs()handlePortChange()
处理端口更改
javascript
/**
* 处理端口更改(异步方法)
* @param {number} oldPort - 旧端口
* @param {number} newPort - 新端口
*/
async handlePortChange(oldPort, newPort)detectEaglePort()
检测Eagle扩展运行端口
javascript
/**
* 检测Eagle扩展运行端口
*/
async detectEaglePort()playConnectionSound()
播放连接音效
javascript
/**
* 播放连接音效(默认启用)
* @param {string} soundType - 音效类型
*/
playConnectionSound(soundType)setupQuickSettings()
设置快速设置管理
javascript
/**
* 设置快速设置管理
*/
setupQuickSettings()updateQuickSetting()
更新快速设置
javascript
/**
* 更新快速设置
* @param {string} fieldPath - 字段路径
* @param {any} value - 值
*/
updateQuickSetting(fieldPath, value)updateQuickSettingsVisibility()
更新快速设置的可见性
javascript
/**
* 更新快速设置的可见性
*/
updateQuickSettingsVisibility()updateLayerOperationButtonsVisual()
更新图层操作按钮的视觉状态
javascript
/**
* 更新图层操作按钮的视觉状态
* @param {string} importBehavior - 导入行为
*/
updateLayerOperationButtonsVisual(importBehavior)loadQuickSettings()
加载快速设置
javascript
/**
* 加载快速设置
*/
loadQuickSettings()syncQuickToAdvanced()
同步快速设置到高级设置
javascript
/**
* 同步快速设置到高级设置
*/
syncQuickToAdvanced()updateModeButtonStyles()
更新模式按钮样式
javascript
/**
* 更新模式按钮样式
*/
updateModeButtonStyles()showCurrentSettings()
显示当前设置状态
javascript
/**
* 显示当前设置状态
*/
showCurrentSettings()debugSettings()
调试设置功能
javascript
/**
* 调试设置功能
*/
debugSettings()continueDebugSettings()
继续调试设置的其余部分
javascript
/**
* 继续调试设置的其余部分
*/
continueDebugSettings()syncSettingsUI()
同步设置UI
javascript
/**
* 同步设置UI
*/
syncSettingsUI()loadExportSettingsToUI()
加载导出设置到UI
javascript
/**
* 加载导出设置到UI
*/
loadExportSettingsToUI()getExportSettingsFromUI()
从UI获取导出设置
javascript
/**
* 从UI获取导出设置(现在直接读取导入模式的设置)
* @returns {Object} 导出设置
*/
getExportSettingsFromUI()updateExportSettingsUI()
更新导出设置UI状态
javascript
/**
* 更新导出设置UI状态
*/
updateExportSettingsUI()handleExportPresets()
导出当前预设为JSON到文档目录的指定子目录
javascript
/**
* 导出当前预设为JSON到文档目录的指定子目录
* @returns {Promise<void>} 无返回值
*/
async handleExportPresets()getUISettingsFromLocalStorage()
从localStorage获取UI面板组设置
javascript
/**
* 从localStorage获取UI面板组设置
* @returns {Object} UI设置对象
*/
getUISettingsFromLocalStorage()getProjectAdjacentSettings()
从localStorage获取项目旁复制设置
javascript
/**
* 从localStorage获取项目旁复制设置
* @returns {Object} 项目旁设置对象
*/
getProjectAdjacentSettings()getCustomFolderSettings()
从localStorage获取自定义文件夹设置
javascript
/**
* 从localStorage获取自定义文件夹设置
* @returns {Object} 自定义文件夹设置对象
*/
getCustomFolderSettings()savePresetsSilently()
静默保存预设到JSON(无弹窗与打开文件夹)
javascript
/**
* 静默保存预设到JSON(无弹窗与打开文件夹)
* @returns {Promise<boolean>} 是否保存成功
*/
async savePresetsSilently()loadPresetsFromDisk()
从JSON自动读取预设并应用到UI
javascript
/**
* 从JSON自动读取预设并应用到UI
* @returns {Promise<void>}
*/
async loadPresetsFromDisk()setupAutoPresetSync()
设置自动预设同步(监听变更并防抖保存)
javascript
/**
* 设置自动预设同步(监听变更并防抖保存)
*/
setupAutoPresetSync()getPresetsBaseFolderPath()
获取当前预设目录(用户自定义的绝对路径)
javascript
/**
* 获取当前预设目录(用户自定义的绝对路径),如果未设置则返回null
* @returns {string|null} 预设目录路径
*/
getPresetsBaseFolderPath()updateOpenPresetsBtnTooltip()
更新"打开预设目录"按钮的悬浮提示为当前目录
javascript
/**
* 更新"打开预设目录"按钮的悬浮提示为当前目录
*/
updateOpenPresetsBtnTooltip()handleOpenPresetsFolder()
打开当前预设目录(如果不存在则创建)
javascript
/**
* 打开当前预设目录(如果不存在则创建)
*/
async handleOpenPresetsFolder()ensurePresetsFolderReady()
确保预设目录存在(启动时调用)
javascript
/**
* 确保预设目录存在(启动时调用)
* @returns {Promise<void>}
*/
async ensurePresetsFolderReady()handleChoosePresetsDirectory()
选择自定义预设目录并保存到偏好中
javascript
/**
* 选择自定义预设目录并保存到偏好中
*/
async handleChoosePresetsDirectory()initPresetFileButtons()
初始化预设文件管理按钮(两种模式通用)
javascript
/**
* 初始化预设文件管理按钮(两种模式通用)
*/
initPresetFileButtons()handleDownloadPreset()
下载预设文件(两种模式自适应)
javascript
/**
* 下载预设文件(两种模式自适应)
*/
handleDownloadPreset()handleOpenPreset()
打开预设文件(两种模式自适应)
javascript
/**
* 打开预设文件(两种模式自适应)
*/
handleOpenPreset()formatTimestamp()
生成时间戳字符串(YYYYMMDD-HHMMSS),用于文件名
javascript
/**
* 生成时间戳字符串(YYYYMMDD-HHMMSS),用于文件名
* @param {Date} date - 时间对象
* @returns {string} 时间戳
*/
formatTimestamp(date)showProjectAdjacentModal()
显示项目旁复制模态框
javascript
/**
* 显示项目旁复制模态框
*/
showProjectAdjacentModal()showCustomFolderModal()
显示自定义文件夹模态框
javascript
/**
* 显示自定义文件夹模态框
*/
showCustomFolderModal()testQuickSettingsEventListeners()
测试快速设置事件监听器
javascript
/**
* 测试快速设置事件监听器
*/
testQuickSettingsEventListeners()forceReinitQuickSettings()
强制重新初始化快速设置(用于调试)
javascript
/**
* 强制重新初始化快速设置(用于调试)
*/
forceReinitQuickSettings()testQuickSettingChange()
手动测试快速设置变化(用于调试)
javascript
/**
* 手动测试快速设置变化(用于调试)
* @param {string} type - 类型
* @param {string} value - 值
*/
testQuickSettingChange(type, value)testAllQuickSettings()
测试所有快速设置选项
javascript
/**
* 测试所有快速设置选项
*/
testAllQuickSettings()diagnoseQuickSettings()
诊断快速设置问题
javascript
/**
* 诊断快速设置问题
*/
diagnoseQuickSettings()rebindQuickSettingsEventListeners()
手动绑定事件监听器(用于修复)
javascript
/**
* 手动绑定事件监听器(用于修复)
*/
rebindQuickSettingsEventListeners()fixAllQuickSettingsIssues()
一键修复所有快速设置问题
javascript
/**
* 一键修复所有快速设置问题
*/
fixAllQuickSettingsIssues()setupDragAndDrop()
设置拖拽监听
javascript
/**
* 设置拖拽监听
*/
setupDragAndDrop()handleDragPreview()
拖拽预检查处理
javascript
/**
* 拖拽预检查处理
* @param {DragEvent} event - 拖拽事件
*/
async handleDragPreview(event)performDragPreviewCheck()
执行拖拽预检查
javascript
/**
* 执行拖拽预检查
* @param {DragEvent} event - 拖拽事件
*/
async performDragPreviewCheck(event)updateDragHint()
更新拖拽提示文本
javascript
/**
* 更新拖拽提示文本
* @param {string} text - 提示文本
* @param {string} type - 提示类型
*/
updateDragHint(text, type = 'default')resetDragPreviewState()
重置拖拽预检查状态
javascript
/**
* 重置拖拽预检查状态
*/
resetDragPreviewState()handleFileDrop()
处理文件拖拽
javascript
/**
* 处理文件拖拽
* @param {DragEvent} event - 拖拽事件
*/
async handleFileDrop(event)handleDirectoryDrop()
处理文件夹拖拽
javascript
/**
* 处理文件夹拖拽
* @param {Array} items - DataTransferItem数组
* @param {Array} files - File数组
*/
async handleDirectoryDrop(items, files)readDirectoryEntry()
递归读取文件夹内容
javascript
/**
* 递归读取文件夹内容
* @param {FileSystemEntry} entry - 文件系统条目
* @returns {Promise<Array>} 文件数组
*/
async readDirectoryEntry(entry)analyzeDroppedFiles()
分析拖拽的文件
javascript
/**
* 分析拖拽的文件
* @param {Array} files - 文件数组
* @returns {Object} 分析结果
*/
analyzeDroppedFiles(files)detectImageSequence()
检测图片序列
javascript
/**
* 检测图片序列
* @param {Array} files - 文件数组
* @returns {Object|null} 序列帧信息或null
*/
detectImageSequence(files)handleFilesDrop()
处理普通文件拖拽
javascript
/**
* 处理普通文件拖拽
* @param {Array} files - 文件数组
* @param {DataTransfer} dataTransfer - 数据传输对象
*/
async handleFilesDrop(files, dataTransfer)isProjectInternalFile()
检查文件是否已在当前AE项目中导入
javascript
/**
* 检查文件是否已在当前AE项目中导入
* @param {Array} files - 文件数组
* @returns {Promise<Object>} 检测结果
*/
async isProjectInternalFile(files)showProjectInternalFileWarning()
显示项目内文件警告提示
javascript
/**
* 显示项目内文件警告提示
* @param {Object} projectFileCheck - 项目文件检测结果
*/
showProjectInternalFileWarning(projectFileCheck)isEagleDrag()
识别Eagle拖拽
javascript
/**
* 识别Eagle拖拽
* @param {DataTransfer} dataTransfer - 数据传输对象
* @param {Array} files - 文件数组
* @returns {boolean} 是否为Eagle拖拽
*/
isEagleDrag(dataTransfer, files)handleEagleDragImport()
处理Eagle拖拽导入
javascript
/**
* 处理Eagle拖拽导入
* @param {Array} files - 文件数组
*/
async handleEagleDragImport(files)handleNonEagleDragImport()
处理非Eagle文件拖拽导入
javascript
/**
* 处理非Eagle文件拖拽导入
* @param {Array} files - 文件数组
*/
async handleNonEagleDragImport(files)showDragHint()
显示拖拽提示
javascript
/**
* 显示拖拽提示
*/
showDragHint()showDragImportStarted()
显示拖拽导入开始提示
javascript
/**
* 显示拖拽导入开始提示
* @param {number} fileCount - 文件数量
*/
showDragImportStarted(fileCount)showDragImportError()
显示拖拽导入错误
javascript
/**
* 显示拖拽导入错误
* @param {string} errorMessage - 错误消息
*/
showDragImportError(errorMessage)showDropMessage()
显示拖拽反馈消息
javascript
/**
* 显示拖拽反馈消息
* @param {string} message - 消息内容
* @param {string} type - 消息类型
*/
showDropMessage(message, type = 'info')getDropMessageIcon()
获取拖拽消息图标
javascript
/**
* 获取拖拽消息图标
* @param {string} type - 消息类型
* @returns {string} 图标
*/
getDropMessageIcon(type)showErrorDialog()
显示错误对话框
javascript
/**
* 显示错误对话框
* @param {string} title - 错误标题
* @param {string} message - 错误消息
*/
showErrorDialog(title, message)getFileIcon()
获取文件图标
javascript
/**
* 获取文件图标
* @param {File} file - 文件对象
* @returns {string} 文件图标
*/
getFileIcon(file)showNonEagleConfirmDialog()
显示非Eagle文件确认对话框
javascript
/**
* 显示非Eagle文件确认对话框
* @param {Array} files - 文件数组
*/
showNonEagleConfirmDialog(files)showEagleConfirmDialog()
显示Eagle确认对话框
javascript
/**
* 显示Eagle确认对话框
* @param {Array} files - 文件数组
*/
showEagleConfirmDialog(files)使用示例
基本使用
javascript
// 初始化剪贴板导入优化功能
const clipboardImport = new OptimizedClipboardImport(aeExtension);
// 设置剪贴板监听
clipboardImport.setupClipboardListener();
// 监听Ctrl+V/Cmd+V事件
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
clipboardImport.handleClipboardPaste(e);
}
});高级使用
javascript
// 自定义剪贴板检测逻辑
clipboardImport.isImportableFile = (file) => {
// 只允许导入PNG和JPG文件
const allowedTypes = ['image/png', 'image/jpeg'];
return allowedTypes.includes(file.type);
};
// 自定义临时文件检测逻辑
clipboardImport.isTemporaryFileEnhanced = (fileName) => {
// 更严格的临时文件检测
const tempPatterns = [
/^clipboard_image\./i,
/^image\./i,
/^screenshot\./i,
/^capture\./i,
/^untitled\./i,
/temp/i,
/tmp/i
];
return tempPatterns.some(pattern => pattern.test(fileName));
};
// 自定义文件名生成逻辑
clipboardImport.generateTimestampFilename = (originalExt) => {
// 使用自定义的时间戳格式
const now = new Date();
const timestamp = now.toISOString().replace(/[:.]/g, '-').replace('T', '_');
return `import_${timestamp}${originalExt}`;
};批量处理
javascript
// 批量处理多个剪贴板导入
async function batchClipboardImport(fileGroups) {
const results = [];
for (const files of fileGroups) {
try {
const result = await clipboardImport.handleClipboardImport(files);
results.push({
files: files.length,
success: true,
result: result
});
} catch (error) {
results.push({
files: files.length,
success: false,
error: error.message
});
}
}
return results;
}
// 使用示例
const fileGroups = [
[file1, file2, file3], // 第一组文件
[file4, file5], // 第二组文件
[file6] // 第三组文件
];
const batchResults = await batchClipboardImport(fileGroups);
console.log('批量剪贴板导入结果:', batchResults);最佳实践
使用建议
文件格式处理
javascript
// 推荐的文件格式处理策略
const recommendedFormats = {
'image/png': true,
'image/jpeg': true,
'image/jpg': true,
'image/gif': true,
'image/bmp': true,
'image/tiff': true,
'image/webp': true,
'image/svg+xml': true
};
// 检查文件格式是否受支持
function isSupportedFormat(file) {
return recommendedFormats[file.type] === true;
}临时文件管理
javascript
// 临时文件命名策略
function generateSmartFilename(originalName, fileType) {
// 如果是临时文件,生成智能名称
if (clipboardImport.isTemporaryFileEnhanced(originalName)) {
const ext = clipboardImport.getFileExtension(originalName);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
return `import_${timestamp}${ext}`;
}
// 如果有原始名称,保持原名
return originalName;
}性能优化
javascript
// 使用防抖处理避免频繁检查
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// 防抖处理剪贴板粘贴
const debouncedClipboardPaste = debounce((event) => {
clipboardImport.handleClipboardPaste(event);
}, 300);内存管理
javascript
// 及时清理事件监听器
function cleanupClipboardListeners() {
// 移除键盘监听器
document.removeEventListener('keydown', handleKeyDown);
// 移除粘贴监听器
document.removeEventListener('paste', handlePaste);
// 清理定时器
if (clipboardImport.clipboardCheckTimeout) {
clearTimeout(clipboardImport.clipboardCheckTimeout);
}
}
// 在组件销毁时调用
window.addEventListener('beforeunload', cleanupClipboardListeners);用户体验优化
视觉反馈
css
/* 剪贴板导入视觉反馈 */
.clipboard-import-indicator {
position: fixed;
top: 20px;
right: 20px;
background: rgba(52, 152, 219, 0.9);
color: white;
padding: 12px 16px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10000;
animation: slideInRight 0.3s ease-out;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.clipboard-import-indicator.success {
background: rgba(46, 204, 113, 0.9);
}
.clipboard-import-indicator.error {
background: rgba(231, 76, 60, 0.9);
}
.clipboard-import-indicator.warning {
background: rgba(241, 196, 15, 0.9);
}交互设计
javascript
// 提供清晰的用户指引
function showClipboardImportGuide() {
const guide = `
📋 剪贴板导入使用指南
1. 在任何地方复制图片 (Ctrl+C / Cmd+C)
2. 回到AE并激活Eagle2Ae扩展面板
3. 按下粘贴快捷键 (Ctrl+V / Cmd+V)
4. 确认导入设置并点击"导入文件"
💡 提示:
- 支持从网页、聊天软件、截图工具等复制图片
- 智能重命名功能会避免使用通用文件名
- 可以在高级设置中自定义导入行为
`;
console.log(guide);
}故障排除
常见问题
剪贴板导入无响应
- 症状:按下Ctrl+V/Cmd+V后无任何反应
- 解决:
- 检查剪贴板监听器是否正确绑定
- 验证扩展面板是否处于激活状态
- 重启AE和扩展面板
文件名重命名失败
- 症状:临时文件名未被正确重命名
- 解决:
- 检查临时文件检测逻辑
- 验证时间戳生成函数
- 检查文件扩展名提取逻辑
图片格式不支持
- 症状:某些格式的图片无法导入
- 解决:
- 检查文件类型检测逻辑
- 添加缺失的格式支持
- 验证MIME类型匹配规则
调试技巧
启用详细日志
javascript
// 在控制台中启用详细日志
localStorage.setItem('debugLogLevel', '0');
// 监控剪贴板事件
document.addEventListener('paste', (e) => {
console.log('📋 粘贴事件触发:', e);
console.log('📋 剪贴板数据类型:', Array.from(e.clipboardData.types));
});
// 监控剪贴板导入过程
window.addEventListener('clipboardImportStart', (e) => {
console.log('📋 剪贴板导入开始:', e.detail);
});
window.addEventListener('clipboardImportComplete', (e) => {
console.log('📋 剪贴板导入完成:', e.detail);
});性能监控
javascript
// 监控剪贴板导入性能
const performanceMarkers = [];
function markClipboardImportStep(step) {
performance.mark(`clipboard-import-${step}`);
performanceMarkers.push(step);
}
function measureClipboardImportPerformance() {
for (let i = 1; i < performanceMarkers.length; i++) {
const start = `clipboard-import-${performanceMarkers[i-1]}`;
const end = `clipboard-import-${performanceMarkers[i]}`;
performance.measure(`clipboard-import-${performanceMarkers[i-1]}-to-${performanceMarkers[i]}`, start, end);
}
const measures = performance.getEntriesByType('measure');
measures.forEach(measure => {
console.log(`⏱️ ${measure.name}: ${measure.duration}ms`);
});
}内存使用监控
javascript
// 监控内存使用情况
function logMemoryUsage() {
if (performance.memory) {
console.log('📋 内存使用情况:', {
used: `${Math.round(performance.memory.usedJSHeapSize / 1024 / 1024)} MB`,
total: `${Math.round(performance.memory.totalJSHeapSize / 1024 / 1024)} MB`,
limit: `${Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)} MB`
});
}
}
// 定期监控内存使用
setInterval(logMemoryUsage, 30000); // 每30秒监控一次扩展性
自定义扩展
扩展剪贴板导入功能
javascript
// 创建自定义剪贴板导入类
class CustomClipboardImport extends OptimizedClipboardImport {
constructor(aeExtension) {
super(aeExtension);
this.customFeatures = new Map();
}
/**
* 注册自定义功能
* @param {string} name - 功能名称
* @param {Function} feature - 功能实现
*/
registerCustomFeature(name, feature) {
this.customFeatures.set(name, feature);
}
/**
* 执行自定义功能
* @param {string} name - 功能名称
* @param {...any} args - 参数
* @returns {any} 功能执行结果
*/
executeCustomFeature(name, ...args) {
const feature = this.customFeatures.get(name);
if (feature && typeof feature === 'function') {
return feature(...args);
}
throw new Error(`未知的自定义功能: ${name}`);
}
}插件化架构
javascript
// 创建剪贴板导入插件
class ClipboardImportPlugin {
constructor(clipboardImport) {
this.clipboardImport = clipboardImport;
this.init();
}
init() {
// 添加自定义事件监听器
this.clipboardImport.addEventListener('clipboardImport', this.handleClipboardImport.bind(this));
// 添加自定义方法
this.clipboardImport.customImport = this.customImport.bind(this);
// 注册自定义文件类型处理
this.registerCustomFileTypeHandlers();
}
/**
* 处理剪贴板导入事件
* @param {CustomEvent} event - 剪贴板导入事件
*/
handleClipboardImport(event) {
const { files, settings } = event.detail;
// 执行插件特定的处理逻辑
this.processImportedFiles(files, settings);
}
/**
* 自定义导入处理
* @param {Array} files - 文件数组
* @param {Object} options - 导入选项
*/
async customImport(files, options) {
// 执行自定义导入逻辑
console.log('执行自定义剪贴板导入:', files, options);
// 调用原始导入方法
const result = await this.clipboardImport.handleClipboardImport(files);
// 执行插件特定的后处理
await this.postProcessImport(result, options);
return result;
}
/**
* 处理导入的文件
* @param {Array} files - 文件数组
* @param {Object} settings - 设置
*/
async processImportedFiles(files, settings) {
// 执行插件特定的文件处理逻辑
for (const file of files) {
await this.processSingleFile(file, settings);
}
}
/**
* 处理单个文件
* @param {File} file - 文件对象
* @param {Object} settings - 设置
*/
async processSingleFile(file, settings) {
// 插件特定的文件处理逻辑
console.log(`处理文件: ${file.name}`, file);
}
/**
* 导入后处理
* @param {Object} result - 导入结果
* @param {Object} options - 导入选项
*/
async postProcessImport(result, options) {
// 执行导入完成后的处理逻辑
console.log('导入后处理:', result, options);
}
/**
* 注册自定义文件类型处理
*/
registerCustomFileTypeHandlers() {
// 注册自定义文件类型处理函数
this.clipboardImport.registerFileTypeHandler('application/custom', (file) => {
// 自定义文件类型处理逻辑
console.log(`处理自定义文件类型: ${file.name}`);
return true; // 返回true表示处理成功
});
}
}
// 应用插件
const plugin = new ClipboardImportPlugin(clipboardImport);