1600 lines
65 KiB
JavaScript
1600 lines
65 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
|
|
var i18n = {
|
|
'zh-CN': {
|
|
searchNodes: '搜索节点',
|
|
extensionSettings: 'MechaBaby 扩展配置',
|
|
portTeleportSettings: '端口传送配置',
|
|
portTeleportFollowLang: '语言跟随节点搜索设置',
|
|
portTeleportNoExtra: '当前无其他可配置项',
|
|
inputPlaceholder: '输入关键词搜索节点名称、ID、属性名称或值...',
|
|
closeButton: '关闭 (Esc)',
|
|
noResults: '未找到匹配的节点',
|
|
foundNodes: '找到 {0} 个节点,{1} 个匹配项',
|
|
nodeTitle: '节点标题',
|
|
nodeType: '节点类型',
|
|
nodeId: '节点ID',
|
|
widget: '控件',
|
|
value: '值',
|
|
property: '属性',
|
|
propertyValue: '属性值',
|
|
unknownNode: '未知节点',
|
|
errorNode: '错误节点',
|
|
nodeMayNotLoaded: '节点可能未正确加载',
|
|
matches: '{0} 个匹配',
|
|
moreMatches: '... 还有 {0} 个匹配项',
|
|
nodeLabel: '节点: ',
|
|
typeLabel: '类型: ',
|
|
nodeIdLabel: 'ID: ',
|
|
widgetLabel: '控件: ',
|
|
valueLabel: '值: ',
|
|
propertyLabel: '属性: ',
|
|
propertyValueLabel: '属性值: ',
|
|
nodeMayNotLoadedHint: ' | ⚠️ 节点可能未正确加载',
|
|
settings: '设置',
|
|
nodeSearchSettings: '节点搜索设置',
|
|
language: '语言',
|
|
shortcut: '快捷键',
|
|
save: '保存',
|
|
cancel: '取消',
|
|
selectLanguage: '选择语言',
|
|
selectShortcut: '设置快捷键',
|
|
currentShortcut: '当前快捷键: ',
|
|
pressKey: '按下您想要的快捷键组合...',
|
|
invalidShortcut: '无效的快捷键,请至少包含 Ctrl/Cmd 和一个按键',
|
|
shortcutSaved: '快捷键已保存',
|
|
languageSaved: '语言已保存,请刷新页面',
|
|
autoDetect: '自动检测',
|
|
chinese: '中文',
|
|
english: 'English',
|
|
japanese: '日本語',
|
|
korean: '한국어',
|
|
russian: 'Русский',
|
|
pinWindow: '钉住窗口',
|
|
unpinWindow: '取消钉住',
|
|
searchMode: '搜索模式',
|
|
normalMode: '普通',
|
|
wildcardMode: '通配符',
|
|
regexMode: '正则'
|
|
},
|
|
'en-US': {
|
|
searchNodes: 'Search Nodes',
|
|
extensionSettings: 'MechaBaby Extension Settings',
|
|
portTeleportSettings: 'Port Teleport Settings',
|
|
portTeleportFollowLang: 'Language follows NodeSearch setting',
|
|
portTeleportNoExtra: 'No additional settings currently',
|
|
inputPlaceholder: 'Enter keywords to search node names, IDs, properties, or values...',
|
|
closeButton: 'Close (Esc)',
|
|
noResults: 'No matching nodes found',
|
|
foundNodes: 'Found {0} nodes, {1} matches',
|
|
nodeTitle: 'Node Title',
|
|
nodeType: 'Node Type',
|
|
nodeId: 'Node ID',
|
|
widget: 'Widget',
|
|
value: 'Value',
|
|
property: 'Property',
|
|
propertyValue: 'Property Value',
|
|
unknownNode: 'Unknown Node',
|
|
errorNode: 'Error Node',
|
|
nodeMayNotLoaded: 'Node may not be loaded correctly',
|
|
matches: '{0} matches',
|
|
moreMatches: '... {0} more matches',
|
|
nodeLabel: 'Node: ',
|
|
typeLabel: 'Type: ',
|
|
nodeIdLabel: 'ID: ',
|
|
widgetLabel: 'Widget: ',
|
|
valueLabel: 'Value: ',
|
|
propertyLabel: 'Property: ',
|
|
propertyValueLabel: 'Property Value: ',
|
|
nodeMayNotLoadedHint: ' | ⚠️ Node may not be loaded correctly',
|
|
settings: 'Settings',
|
|
nodeSearchSettings: 'NodeSearch Settings',
|
|
language: 'Language',
|
|
shortcut: 'Shortcut',
|
|
save: 'Save',
|
|
cancel: 'Cancel',
|
|
selectLanguage: 'Select Language',
|
|
selectShortcut: 'Set Shortcut',
|
|
currentShortcut: 'Current Shortcut: ',
|
|
pressKey: 'Press your desired shortcut combination...',
|
|
invalidShortcut: 'Invalid shortcut, please include at least Ctrl/Cmd and one key',
|
|
shortcutSaved: 'Shortcut saved',
|
|
languageSaved: 'Language saved, please refresh the page',
|
|
autoDetect: 'Auto Detect',
|
|
chinese: '中文',
|
|
english: 'English',
|
|
japanese: '日本語',
|
|
korean: '한국어',
|
|
russian: 'Русский',
|
|
pinWindow: 'Pin Window',
|
|
unpinWindow: 'Unpin Window',
|
|
searchMode: 'Search Mode',
|
|
normalMode: 'Normal',
|
|
wildcardMode: 'Wildcard',
|
|
regexMode: 'Regex'
|
|
},
|
|
'ja-JP': {
|
|
searchNodes: 'ノード検索',
|
|
extensionSettings: 'MechaBaby 拡張設定',
|
|
portTeleportSettings: 'ポートテレポート設定',
|
|
portTeleportFollowLang: '言語は NodeSearch 設定に従います',
|
|
portTeleportNoExtra: '現在追加の設定はありません',
|
|
inputPlaceholder: 'キーワードを入力してノード名、ID、プロパティまたは値を検索...',
|
|
closeButton: '閉じる (Esc)',
|
|
noResults: '一致するノードが見つかりません',
|
|
foundNodes: '{0} 個のノード、{1} 件の一致',
|
|
nodeTitle: 'ノードタイトル',
|
|
nodeType: 'ノードタイプ',
|
|
nodeId: 'ノードID',
|
|
widget: 'ウィジェット',
|
|
value: '値',
|
|
property: 'プロパティ',
|
|
propertyValue: 'プロパティ値',
|
|
unknownNode: '不明なノード',
|
|
errorNode: 'エラーノード',
|
|
nodeMayNotLoaded: 'ノードが正しく読み込まれていない可能性があります',
|
|
matches: '{0} 件の一致',
|
|
moreMatches: '... さらに {0} 件の一致',
|
|
nodeLabel: 'ノード: ',
|
|
typeLabel: 'タイプ: ',
|
|
nodeIdLabel: 'ID: ',
|
|
widgetLabel: 'ウィジェット: ',
|
|
valueLabel: '値: ',
|
|
propertyLabel: 'プロパティ: ',
|
|
propertyValueLabel: 'プロパティ値: ',
|
|
nodeMayNotLoadedHint: ' | ⚠️ ノードが正しく読み込まれていない可能性があります',
|
|
shadowBlur: '1px solid rgba(255, 255, 255, 0.2)',
|
|
settings: '設定',
|
|
nodeSearchSettings: 'NodeSearch 設定',
|
|
language: '言語',
|
|
shortcut: 'ショートカット',
|
|
save: '保存',
|
|
cancel: 'キャンセル',
|
|
selectLanguage: '言語を選択',
|
|
selectShortcut: 'ショートカットを設定',
|
|
currentShortcut: '現在のショートカット: ',
|
|
pressKey: '希望のショートカットキーの組み合わせを押してください...',
|
|
invalidShortcut: '無効なショートカットです。Ctrl/Cmd と少なくとも 1 つのキーを含めてください',
|
|
shortcutSaved: 'ショートカットを保存しました',
|
|
languageSaved: '言語を保存しました。ページを更新してください',
|
|
autoDetect: '自動検出',
|
|
chinese: '中文',
|
|
english: 'English',
|
|
japanese: '日本語',
|
|
korean: '한국어',
|
|
russian: 'Русский',
|
|
pinWindow: 'ウィンドウを固定',
|
|
unpinWindow: '固定を解除',
|
|
searchMode: '検索モード',
|
|
normalMode: '通常',
|
|
wildcardMode: 'ワイルドカード',
|
|
regexMode: '正規表現'
|
|
},
|
|
'ko-KR': {
|
|
searchNodes: '노드 검색',
|
|
extensionSettings: 'MechaBaby 확장 설정',
|
|
portTeleportSettings: '포트 텔레포트 설정',
|
|
portTeleportFollowLang: '언어는 NodeSearch 설정을 따릅니다',
|
|
portTeleportNoExtra: '추가 설정 없음',
|
|
inputPlaceholder: '키워드를 입력하여 노드 이름, ID, 속성 또는 값을 검색...',
|
|
closeButton: '닫기 (Esc)',
|
|
noResults: '일치하는 노드를 찾을 수 없습니다',
|
|
foundNodes: '{0}개의 노드, {1}개의 일치 항목',
|
|
nodeTitle: '노드 제목',
|
|
nodeType: '노드 유형',
|
|
nodeId: '노드 ID',
|
|
widget: '위젯',
|
|
value: '값',
|
|
property: '속성',
|
|
propertyValue: '속성 값',
|
|
unknownNode: '알 수 없는 노드',
|
|
errorNode: '오류 노드',
|
|
nodeMayNotLoaded: '노드가 제대로 로드되지 않았을 수 있습니다',
|
|
matches: '{0}개의 일치',
|
|
moreMatches: '... {0}개의 추가 일치',
|
|
nodeLabel: '노드: ',
|
|
typeLabel: '유형: ',
|
|
nodeIdLabel: 'ID: ',
|
|
widgetLabel: '위젯: ',
|
|
valueLabel: '값: ',
|
|
propertyLabel: '속성: ',
|
|
propertyValueLabel: '속성 값: ',
|
|
nodeMayNotLoadedHint: ' | ⚠️ 노드가 제대로 로드되지 않았을 수 있습니다',
|
|
settings: '설정',
|
|
nodeSearchSettings: 'NodeSearch 설정',
|
|
language: '언어',
|
|
shortcut: '단축키',
|
|
save: '저장',
|
|
cancel: '취소',
|
|
selectLanguage: '언어 선택',
|
|
selectShortcut: '단축키 설정',
|
|
currentShortcut: '현재 단축키: ',
|
|
pressKey: '원하는 단축키 조합을 누르세요...',
|
|
invalidShortcut: '잘못된 단축키입니다. Ctrl/Cmd와 최소 하나의 키를 포함하세요',
|
|
shortcutSaved: '단축키가 저장되었습니다',
|
|
languageSaved: '언어가 저장되었습니다. 페이지를 새로고침하세요',
|
|
autoDetect: '자동 감지',
|
|
chinese: '中文',
|
|
english: 'English',
|
|
japanese: '日本語',
|
|
korean: '한국어',
|
|
russian: 'Русский',
|
|
pinWindow: '창 고정',
|
|
unpinWindow: '고정 해제',
|
|
searchMode: '검색 모드',
|
|
normalMode: '일반',
|
|
wildcardMode: '와일드카드',
|
|
regexMode: '정규식'
|
|
},
|
|
'ru-RU': {
|
|
searchNodes: 'Поиск узлов',
|
|
extensionSettings: 'Настройки MechaBaby',
|
|
portTeleportSettings: 'Настройки Port Teleport',
|
|
portTeleportFollowLang: 'Язык следует настройке NodeSearch',
|
|
portTeleportNoExtra: 'Дополнительных настроек пока нет',
|
|
inputPlaceholder: 'Введите ключевые слова для поиска имен узлов, ID, свойств или значений...',
|
|
closeButton: 'Закрыть (Esc)',
|
|
noResults: 'Совпадающие узлы не найдены',
|
|
foundNodes: 'Найдено {0} узлов, {1} совпадений',
|
|
nodeTitle: 'Заголовок узла',
|
|
nodeType: 'Тип узла',
|
|
nodeId: 'ID узла',
|
|
widget: 'Виджет',
|
|
value: 'Значение',
|
|
property: 'Свойство',
|
|
propertyValue: 'Значение свойства',
|
|
unknownNode: 'Неизвестный узел',
|
|
errorNode: 'Ошибочный узел',
|
|
nodeMayNotLoaded: 'Узел может быть загружен неправильно',
|
|
matches: '{0} совпадений',
|
|
moreMatches: '... еще {0} совпадений',
|
|
nodeLabel: 'Узел: ',
|
|
typeLabel: 'Тип: ',
|
|
nodeIdLabel: 'ID: ',
|
|
widgetLabel: 'Виджет: ',
|
|
valueLabel: 'Значение: ',
|
|
propertyLabel: 'Свойство: ',
|
|
propertyValueLabel: 'Значение свойства: ',
|
|
nodeMayNotLoadedHint: ' | ⚠️ Узел может быть загружен неправильно',
|
|
settings: 'Настройки',
|
|
nodeSearchSettings: 'NodeSearch Настройки',
|
|
language: 'Язык',
|
|
shortcut: 'Горячая клавиша',
|
|
save: 'Сохранить',
|
|
cancel: 'Отмена',
|
|
selectLanguage: 'Выберите язык',
|
|
selectShortcut: 'Установить горячую клавишу',
|
|
currentShortcut: 'Текущая горячая клавиша: ',
|
|
pressKey: 'Нажмите желаемую комбинацию клавиш...',
|
|
invalidShortcut: 'Неверная горячая клавиша, пожалуйста, включите как минимум Ctrl/Cmd и одну клавишу',
|
|
shortcutSaved: 'Горячая клавиша сохранена',
|
|
languageSaved: 'Язык сохранен, пожалуйста, обновите страницу',
|
|
autoDetect: 'Автоопределение',
|
|
chinese: '中文',
|
|
english: 'English',
|
|
japanese: '日本語',
|
|
korean: '한국어',
|
|
russian: 'Русский',
|
|
pinWindow: 'Закрепить окно',
|
|
unpinWindow: 'Открепить окно',
|
|
searchMode: 'Режим поиска',
|
|
normalMode: 'Обычный',
|
|
wildcardMode: 'Подстановочные знаки',
|
|
regexMode: 'Регулярное выражение'
|
|
}
|
|
};
|
|
|
|
|
|
var langMap = {
|
|
'zh': 'zh-CN',
|
|
'zh-CN': 'zh-CN',
|
|
'zh-TW': 'zh-CN',
|
|
'en': 'en-US',
|
|
'en-US': 'en-US',
|
|
'en-GB': 'en-US',
|
|
'ja': 'ja-JP',
|
|
'ja-JP': 'ja-JP',
|
|
'ko': 'ko-KR',
|
|
'ko-KR': 'ko-KR',
|
|
'ru': 'ru-RU',
|
|
'ru-RU': 'ru-RU'
|
|
};
|
|
|
|
|
|
function showToast(message, duration) {
|
|
duration = duration || 2000;
|
|
var toast = document.createElement("div");
|
|
toast.style.cssText =
|
|
"position: fixed;" +
|
|
"top: 20px;" +
|
|
"right: 20px;" +
|
|
"background: rgba(255, 193, 7, 0.9);" +
|
|
"backdrop-filter: blur(10px);" +
|
|
"color: #0c0c0c;" +
|
|
"padding: 16px 26px;" +
|
|
"border-radius: 8px;" +
|
|
"box-shadow: 0 6px 20px rgba(255, 193, 7, 0.5);" +
|
|
"border: 1px solid rgba(255, 235, 59, 0.6);" +
|
|
"z-index: 10001;" +
|
|
"font-size: 18px;" +
|
|
"font-weight: 600;" +
|
|
"max-width: 400px;" +
|
|
"word-wrap: break-word;" +
|
|
"opacity: 0;" +
|
|
"transform: translateY(-10px);" +
|
|
"transition: opacity 0.3s ease, transform 0.3s ease;";
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(function() {
|
|
toast.style.opacity = "1";
|
|
toast.style.transform = "translateY(0)";
|
|
}, 10);
|
|
|
|
setTimeout(function() {
|
|
toast.style.opacity = "0";
|
|
toast.style.transform = "translateY(-10px)";
|
|
setTimeout(function() {
|
|
if (toast.parentNode) {
|
|
toast.parentNode.removeChild(toast);
|
|
}
|
|
}, 300);
|
|
}, duration);
|
|
}
|
|
|
|
var config = {
|
|
|
|
getLanguage: function() {
|
|
var saved = localStorage.getItem('mechababy.nodeSearch.language');
|
|
if (saved && i18n[saved]) {
|
|
return saved;
|
|
}
|
|
|
|
var browserLang = navigator.language || navigator.userLanguage || 'en-US';
|
|
return langMap[browserLang] || langMap[browserLang.split('-')[0]] || 'en-US';
|
|
},
|
|
|
|
setLanguage: function(lang) {
|
|
if (i18n[lang]) {
|
|
localStorage.setItem('mechababy.nodeSearch.language', lang);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
getShortcut: function() {
|
|
var saved = localStorage.getItem('mechababy.nodeSearch.shortcut');
|
|
if (saved) {
|
|
try {
|
|
return JSON.parse(saved);
|
|
} catch (e) {
|
|
return { ctrl: true, key: 'f' };
|
|
}
|
|
}
|
|
return { ctrl: true, key: 'f' };
|
|
},
|
|
|
|
setShortcut: function(shortcut) {
|
|
try {
|
|
localStorage.setItem('mechababy.nodeSearch.shortcut', JSON.stringify(shortcut));
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
formatShortcut: function(shortcut) {
|
|
if (!shortcut) shortcut = this.getShortcut();
|
|
var parts = [];
|
|
if (shortcut.ctrl) parts.push('Ctrl');
|
|
if (shortcut.alt) parts.push('Alt');
|
|
if (shortcut.shift) parts.push('Shift');
|
|
if (shortcut.meta) parts.push('Cmd');
|
|
if (shortcut.key) {
|
|
var key = shortcut.key;
|
|
if (key.length === 1) {
|
|
key = key.toUpperCase();
|
|
}
|
|
parts.push(key);
|
|
}
|
|
return parts.join('+');
|
|
},
|
|
|
|
getIsPinned: function() {
|
|
return localStorage.getItem('mechababy.nodeSearch.pinned') === 'true';
|
|
},
|
|
|
|
setIsPinned: function(pinned) {
|
|
localStorage.setItem('mechababy.nodeSearch.pinned', pinned);
|
|
},
|
|
|
|
getPinnedPosition: function() {
|
|
var saved = localStorage.getItem('mechababy.nodeSearch.pinnedPosition');
|
|
if (saved) {
|
|
try {
|
|
return JSON.parse(saved);
|
|
} catch (e) {}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
setPinnedPosition: function(pos) {
|
|
localStorage.setItem('mechababy.nodeSearch.pinnedPosition', JSON.stringify(pos));
|
|
},
|
|
|
|
getSearchMode: function() {
|
|
var saved = localStorage.getItem('mechababy.nodeSearch.searchMode');
|
|
if (saved === 'wildcard' || saved === 'regex') {
|
|
return saved;
|
|
}
|
|
return 'normal';
|
|
},
|
|
|
|
setSearchMode: function(mode) {
|
|
if (mode === 'normal' || mode === 'wildcard' || mode === 'regex') {
|
|
localStorage.setItem('mechababy.nodeSearch.searchMode', mode);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
function t(key) {
|
|
var lang = config.getLanguage();
|
|
var texts = i18n[lang] || i18n['en-US'];
|
|
var text = texts[key] || i18n['en-US'][key] || key;
|
|
|
|
if (arguments.length > 1) {
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
text = text.replace('{' + (i - 1) + '}', arguments[i]);
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
|
|
var nodeSearchState = {
|
|
searchDialog: null,
|
|
searchResults: [],
|
|
currentResultIndex: -1,
|
|
keyboardHandlerBound: false,
|
|
openSearchDialog: null,
|
|
currentHandler: null,
|
|
settingsDialog: null,
|
|
isPinned: false,
|
|
pinnedPosition: null
|
|
};
|
|
|
|
app.registerExtension({
|
|
name: "MechaBaby.NodeSearch",
|
|
|
|
|
|
getCanvasMenuItems: function() {
|
|
var currentShortcut = config.formatShortcut();
|
|
var menuItems = [
|
|
null,
|
|
{
|
|
content: '🔍 ' + t('searchNodes') + ' (' + currentShortcut + ')',
|
|
callback: function() {
|
|
if (nodeSearchState.openSearchDialog) {
|
|
nodeSearchState.openSearchDialog();
|
|
}
|
|
}
|
|
},
|
|
null,
|
|
{
|
|
content: '⚙️ ' + t('extensionSettings'),
|
|
has_submenu: true,
|
|
submenu: {
|
|
options: [
|
|
{
|
|
content: '🔍 ' + t('nodeSearchSettings'),
|
|
has_submenu: true,
|
|
submenu: {
|
|
options: [
|
|
{
|
|
content: t('language'),
|
|
has_submenu: true,
|
|
submenu: {
|
|
options: [
|
|
{ content: t('autoDetect'), callback: function() { config.setLanguage(config.getLanguage()); } },
|
|
null,
|
|
{ content: '🇨🇳 ' + t('chinese'), callback: function() { config.setLanguage('zh-CN'); showToast(t('languageSaved')); } },
|
|
{ content: '🇺🇸 ' + t('english'), callback: function() { config.setLanguage('en-US'); showToast(t('languageSaved')); } },
|
|
{ content: '🇯🇵 ' + t('japanese'), callback: function() { config.setLanguage('ja-JP'); showToast(t('languageSaved')); } },
|
|
{ content: '🇰🇷 ' + t('korean'), callback: function() { config.setLanguage('ko-KR'); showToast(t('languageSaved')); } },
|
|
{ content: '🇷🇺 ' + t('russian'), callback: function() { config.setLanguage('ru-RU'); showToast(t('languageSaved')); } }
|
|
]
|
|
}
|
|
},
|
|
{
|
|
content: t('shortcut'),
|
|
callback: function() {
|
|
if (nodeSearchState.openSettingsDialog) {
|
|
nodeSearchState.openSettingsDialog();
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
];
|
|
|
|
// 合并端口传送的菜单项(如果存在)
|
|
if (typeof window !== 'undefined' && window.getPortTeleportMenuItems) {
|
|
var portTeleportItems = window.getPortTeleportMenuItems();
|
|
if (portTeleportItems && portTeleportItems.length > 0) {
|
|
for (var i = 0; i < menuItems.length; i++) {
|
|
if (menuItems[i] && menuItems[i].content && (menuItems[i].content.indexOf('MechaBaby') >= 0 || menuItems[i].content.indexOf(t('extensionSettings')) >= 0)) {
|
|
if (menuItems[i].submenu && menuItems[i].submenu.options) {
|
|
menuItems[i].submenu.options.push(null);
|
|
menuItems[i].submenu.options = menuItems[i].submenu.options.concat(portTeleportItems);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return menuItems;
|
|
},
|
|
|
|
setup: function() {
|
|
var searchDialog = nodeSearchState.searchDialog;
|
|
var searchResults = nodeSearchState.searchResults;
|
|
var currentResultIndex = nodeSearchState.currentResultIndex;
|
|
var keyboardHandlerBound = nodeSearchState.keyboardHandlerBound;
|
|
|
|
|
|
function textMatchesSearch(text, keyword, mode) {
|
|
if (text === null || text === undefined) return false;
|
|
var str = String(text);
|
|
if (!keyword || keyword.trim() === "") return false;
|
|
var kw = keyword.trim();
|
|
|
|
if (mode === 'regex') {
|
|
try {
|
|
var re = new RegExp(kw, 'i');
|
|
return re.test(str);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
if (mode === 'wildcard') {
|
|
try {
|
|
var escaped = kw.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
escaped = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
var re = new RegExp(escaped, 'i');
|
|
return re.test(str);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
return str.toLowerCase().includes(kw.toLowerCase());
|
|
}
|
|
|
|
function searchNodes(keyword) {
|
|
if (!keyword || keyword.trim() === "") {
|
|
return [];
|
|
}
|
|
|
|
var searchMode = config.getSearchMode();
|
|
var results = [];
|
|
|
|
|
|
if (app.graph && app.graph._nodes) {
|
|
app.graph._nodes.forEach(function(node) {
|
|
|
|
try {
|
|
|
|
if (!node) return;
|
|
|
|
const matches = [];
|
|
let nodeTitle = "";
|
|
|
|
|
|
try {
|
|
nodeTitle = node.getTitle ? node.getTitle() : (node.title || node.type || "");
|
|
} catch (e) {
|
|
nodeTitle = node.type || node.title || t('unknownNode');
|
|
}
|
|
|
|
|
|
if (nodeTitle && textMatchesSearch(nodeTitle, keyword, searchMode)) {
|
|
matches.push({
|
|
type: 'node_title',
|
|
name: t('nodeTitle'),
|
|
value: nodeTitle,
|
|
display: t('nodeLabel') + nodeTitle
|
|
});
|
|
}
|
|
|
|
|
|
if (node.type && textMatchesSearch(node.type, keyword, searchMode)) {
|
|
matches.push({
|
|
type: 'node_type',
|
|
name: t('nodeType'),
|
|
value: node.type,
|
|
display: t('typeLabel') + node.type
|
|
});
|
|
}
|
|
|
|
|
|
if (node.id !== undefined && node.id !== null) {
|
|
var nodeIdStr = String(node.id);
|
|
if (textMatchesSearch(nodeIdStr, keyword, searchMode)) {
|
|
matches.push({
|
|
type: 'node_id',
|
|
name: t('nodeId'),
|
|
value: node.id,
|
|
display: t('nodeIdLabel') + nodeIdStr
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
try {
|
|
if (node.widgets && Array.isArray(node.widgets)) {
|
|
node.widgets.forEach(function(widget, index) {
|
|
try {
|
|
if (!widget) return;
|
|
|
|
const widgetName = widget.name || '';
|
|
const widgetValue = (node.widgets_values && node.widgets_values[index] !== undefined)
|
|
? node.widgets_values[index]
|
|
: (widget.value !== undefined ? widget.value : '');
|
|
|
|
|
|
if (widgetName && textMatchesSearch(widgetName, keyword, searchMode)) {
|
|
matches.push({
|
|
type: 'widget_name',
|
|
name: widgetName,
|
|
value: widgetValue,
|
|
display: t('widgetLabel') + widgetName + ' = ' + String(widgetValue).substring(0, 50)
|
|
});
|
|
}
|
|
|
|
|
|
const valueStr = String(widgetValue);
|
|
if (valueStr && textMatchesSearch(valueStr, keyword, searchMode) && widgetName) {
|
|
matches.push({
|
|
type: 'widget_value',
|
|
name: widgetName,
|
|
value: widgetValue,
|
|
display: t('valueLabel') + widgetName + ' = ' + valueStr.substring(0, 50)
|
|
});
|
|
}
|
|
} catch (widgetError) {
|
|
|
|
console.debug("[MechaBaby NodeSearch] 搜索控件时出�?", widgetError);
|
|
}
|
|
});
|
|
}
|
|
} catch (widgetsError) {
|
|
|
|
console.debug("[MechaBaby NodeSearch] 访问节点控件时出�?", widgetsError);
|
|
}
|
|
|
|
|
|
try {
|
|
if (node.properties && typeof node.properties === 'object') {
|
|
Object.keys(node.properties).forEach(function(propName) {
|
|
try {
|
|
var propValue = node.properties[propName];
|
|
var propValueStr = String(propValue);
|
|
|
|
|
|
if (propName && textMatchesSearch(propName, keyword, searchMode)) {
|
|
matches.push({
|
|
type: 'property_name',
|
|
name: propName,
|
|
value: propValue,
|
|
display: t('propertyLabel') + propName + ' = ' + propValueStr.substring(0, 50)
|
|
});
|
|
}
|
|
|
|
|
|
if (propValueStr && textMatchesSearch(propValueStr, keyword, searchMode)) {
|
|
matches.push({
|
|
type: 'property_value',
|
|
name: propName,
|
|
value: propValue,
|
|
display: t('propertyValueLabel') + propName + ' = ' + propValueStr.substring(0, 50)
|
|
});
|
|
}
|
|
} catch (propError) {
|
|
|
|
console.debug("[MechaBaby NodeSearch] 搜索属性时出错:", propError);
|
|
}
|
|
});
|
|
}
|
|
} catch (propertiesError) {
|
|
|
|
console.debug("[MechaBaby NodeSearch] 访问节点属性时出错:", propertiesError);
|
|
}
|
|
|
|
|
|
if (matches.length > 0) {
|
|
results.push({
|
|
node: node,
|
|
nodeTitle: nodeTitle,
|
|
nodeType: node.type || "未知类型",
|
|
nodeId: node.id,
|
|
matches: matches,
|
|
matchCount: matches.length,
|
|
isAvailableNode: false
|
|
});
|
|
}
|
|
} catch (nodeError) {
|
|
|
|
const nodeTypeStr = (node && node.type) ? node.type : "未知";
|
|
console.debug("[MechaBaby NodeSearch] 搜索节点时出错(可能是未安装的节点):", nodeError, nodeTypeStr);
|
|
|
|
if (node && node.type) {
|
|
try {
|
|
const nodeType = node.type;
|
|
if (textMatchesSearch(nodeType, keyword, searchMode)) {
|
|
results.push({
|
|
node: node,
|
|
nodeTitle: nodeType + ' (' + t('errorNode') + ')',
|
|
nodeType: nodeType,
|
|
nodeId: node.id,
|
|
matches: [{
|
|
type: 'node_type',
|
|
name: t('nodeType'),
|
|
value: nodeType,
|
|
display: t('typeLabel') + nodeType + ' (' + t('nodeMayNotLoaded') + ')'
|
|
}],
|
|
matchCount: 1,
|
|
isAvailableNode: false,
|
|
hasError: true
|
|
});
|
|
}
|
|
} catch (e) {
|
|
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
|
|
function jumpToNode(node, matchIndex) {
|
|
if (matchIndex === undefined) {
|
|
matchIndex = 0;
|
|
}
|
|
if (!node) return;
|
|
|
|
|
|
app.canvas.centerOnNode(node);
|
|
|
|
|
|
app.canvas.selectNode(node);
|
|
|
|
|
|
highlightNode(node);
|
|
}
|
|
|
|
|
|
function highlightNode(node) {
|
|
if (!node) return;
|
|
|
|
|
|
var originalColor = node.color;
|
|
var originalBgColor = node.bgcolor;
|
|
|
|
|
|
var highlightColor = "#FFD700";
|
|
var highlightBgColor = "#4a3d00";
|
|
|
|
var flashCount = 0;
|
|
var maxFlashes = 6;
|
|
var flashInterval = 150;
|
|
|
|
function flash() {
|
|
if (flashCount >= maxFlashes) {
|
|
|
|
node.color = originalColor;
|
|
node.bgcolor = originalBgColor;
|
|
app.canvas.setDirty(true, true);
|
|
return;
|
|
}
|
|
|
|
if (flashCount % 2 === 0) {
|
|
|
|
node.color = highlightColor;
|
|
node.bgcolor = highlightBgColor;
|
|
} else {
|
|
|
|
node.color = originalColor;
|
|
node.bgcolor = originalBgColor;
|
|
}
|
|
|
|
app.canvas.setDirty(true, true);
|
|
flashCount++;
|
|
setTimeout(flash, flashInterval);
|
|
}
|
|
|
|
|
|
flash();
|
|
}
|
|
|
|
|
|
function createSearchDialog() {
|
|
if (searchDialog) {
|
|
return searchDialog;
|
|
}
|
|
|
|
nodeSearchState.isPinned = config.getIsPinned();
|
|
nodeSearchState.pinnedPosition = config.getPinnedPosition();
|
|
|
|
const dialog = document.createElement('div');
|
|
dialog.id = 'mechababy-node-search-dialog';
|
|
|
|
var initialStyle =
|
|
'position: fixed;' +
|
|
'background: #2a2a2a;' +
|
|
'border: 2px solid #4a4a4a;' +
|
|
'border-radius: 8px;' +
|
|
'padding: 20px;' +
|
|
'z-index: 10000;' +
|
|
'min-width: 500px;' +
|
|
'max-width: 700px;' +
|
|
'max-height: 600px;' +
|
|
'display: flex;' +
|
|
'flex-direction: column;' +
|
|
'box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);' +
|
|
"font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif;";
|
|
|
|
if (nodeSearchState.isPinned && nodeSearchState.pinnedPosition) {
|
|
initialStyle += 'top: ' + nodeSearchState.pinnedPosition.y + 'px; left: ' + nodeSearchState.pinnedPosition.x + 'px; transform: none;';
|
|
} else {
|
|
initialStyle += 'top: 50%; left: 50%; transform: translate(-50%, -50%);';
|
|
}
|
|
dialog.style.cssText = initialStyle;
|
|
|
|
|
|
const header = document.createElement('div');
|
|
header.style.cssText =
|
|
'display: flex;' +
|
|
'justify-content: space-between;' +
|
|
'align-items: center;' +
|
|
'margin-bottom: 15px;' +
|
|
'border-bottom: 1px solid #4a4a4a;' +
|
|
'padding-bottom: 10px;' +
|
|
'cursor: move;' +
|
|
'user-select: none;';
|
|
|
|
const title = document.createElement('div');
|
|
title.textContent = '🔍 ' + t('searchNodes');
|
|
title.style.cssText =
|
|
'font-size: 18px;' +
|
|
'font-weight: bold;' +
|
|
'color: #e0e0e0;';
|
|
header.appendChild(title);
|
|
|
|
const headerButtons = document.createElement('div');
|
|
headerButtons.style.cssText = 'display: flex; align-items: center; gap: 8px;';
|
|
|
|
const pinBtn = document.createElement('span');
|
|
pinBtn.textContent = nodeSearchState.isPinned ? "📌" : "📍";
|
|
pinBtn.title = nodeSearchState.isPinned ? t('unpinWindow') : t('pinWindow');
|
|
pinBtn.style.cssText =
|
|
'cursor: pointer;' +
|
|
'font-size: 16px;' +
|
|
'color: ' + (nodeSearchState.isPinned ? '#4a9eff' : '#888') + ';' +
|
|
'width: 24px;' +
|
|
'height: 24px;' +
|
|
'display: flex;' +
|
|
'align-items: center;' +
|
|
'justify-content: center;' +
|
|
'border-radius: 4px;' +
|
|
'transition: background 0.2s;';
|
|
|
|
pinBtn.addEventListener('mouseenter', function() {
|
|
pinBtn.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
});
|
|
pinBtn.addEventListener('mouseleave', function() {
|
|
pinBtn.style.background = 'transparent';
|
|
});
|
|
|
|
pinBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
nodeSearchState.isPinned = !nodeSearchState.isPinned;
|
|
config.setIsPinned(nodeSearchState.isPinned);
|
|
|
|
if (nodeSearchState.isPinned) {
|
|
var rect = dialog.getBoundingClientRect();
|
|
nodeSearchState.pinnedPosition = { x: rect.left, y: rect.top };
|
|
config.setPinnedPosition(nodeSearchState.pinnedPosition);
|
|
|
|
dialog.style.transform = 'none';
|
|
dialog.style.top = nodeSearchState.pinnedPosition.y + 'px';
|
|
dialog.style.left = nodeSearchState.pinnedPosition.x + 'px';
|
|
} else {
|
|
dialog.style.top = '50%';
|
|
dialog.style.left = '50%';
|
|
dialog.style.transform = 'translate(-50%, -50%)';
|
|
}
|
|
|
|
pinBtn.textContent = nodeSearchState.isPinned ? "📌" : "📍";
|
|
pinBtn.title = nodeSearchState.isPinned ? t('unpinWindow') : t('pinWindow');
|
|
pinBtn.style.color = nodeSearchState.isPinned ? '#4a9eff' : '#888';
|
|
});
|
|
|
|
headerButtons.appendChild(pinBtn);
|
|
header.appendChild(headerButtons);
|
|
dialog.appendChild(header);
|
|
|
|
|
|
var isDragging = false;
|
|
var dragStartX, dragStartY;
|
|
var dialogStartX, dialogStartY;
|
|
|
|
header.addEventListener('mousedown', function(e) {
|
|
if (e.target === pinBtn) return;
|
|
|
|
isDragging = true;
|
|
dragStartX = e.clientX;
|
|
dragStartY = e.clientY;
|
|
|
|
var rect = dialog.getBoundingClientRect();
|
|
dialogStartX = rect.left;
|
|
dialogStartY = rect.top;
|
|
|
|
|
|
if (dialog.style.transform && dialog.style.transform.includes('translate')) {
|
|
dialog.style.transform = 'none';
|
|
dialog.style.left = dialogStartX + 'px';
|
|
dialog.style.top = dialogStartY + 'px';
|
|
}
|
|
|
|
document.addEventListener('mousemove', onMouseMove);
|
|
document.addEventListener('mouseup', onMouseUp);
|
|
|
|
e.preventDefault();
|
|
});
|
|
|
|
function onMouseMove(e) {
|
|
if (!isDragging) return;
|
|
|
|
var dx = e.clientX - dragStartX;
|
|
var dy = e.clientY - dragStartY;
|
|
|
|
var newX = dialogStartX + dx;
|
|
var newY = dialogStartY + dy;
|
|
|
|
dialog.style.left = newX + 'px';
|
|
dialog.style.top = newY + 'px';
|
|
}
|
|
|
|
function onMouseUp() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
|
|
if (nodeSearchState.isPinned) {
|
|
var rect = dialog.getBoundingClientRect();
|
|
nodeSearchState.pinnedPosition = { x: rect.left, y: rect.top };
|
|
config.setPinnedPosition(nodeSearchState.pinnedPosition);
|
|
}
|
|
|
|
document.removeEventListener('mousemove', onMouseMove);
|
|
document.removeEventListener('mouseup', onMouseUp);
|
|
}
|
|
}
|
|
|
|
|
|
const inputContainer = document.createElement('div');
|
|
inputContainer.style.cssText = 'margin-bottom: 15px;';
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.placeholder = t('inputPlaceholder');
|
|
input.style.cssText =
|
|
'width: 100%;' +
|
|
'padding: 10px;' +
|
|
'background: #1a1a1a;' +
|
|
'border: 1px solid #4a4a4a;' +
|
|
'border-radius: 4px;' +
|
|
'color: #e0e0e0;' +
|
|
'font-size: 14px;' +
|
|
'box-sizing: border-box;';
|
|
|
|
input.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (searchResults.length > 0 && currentResultIndex >= 0) {
|
|
var result = searchResults[currentResultIndex];
|
|
jumpToNode(result.node);
|
|
if (!nodeSearchState.isPinned) {
|
|
closeDialog();
|
|
}
|
|
}
|
|
} else if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
if (currentResultIndex < searchResults.length - 1) {
|
|
currentResultIndex++;
|
|
updateResultsList();
|
|
}
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (currentResultIndex > 0) {
|
|
currentResultIndex--;
|
|
updateResultsList();
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
closeDialog();
|
|
}
|
|
});
|
|
input.addEventListener('input', function(e) {
|
|
var keyword = e.target.value;
|
|
searchResults = searchNodes(keyword);
|
|
currentResultIndex = searchResults.length > 0 ? 0 : -1;
|
|
updateResultsList();
|
|
});
|
|
inputContainer.appendChild(input);
|
|
|
|
var modeRow = document.createElement('div');
|
|
modeRow.style.cssText = 'display: flex; align-items: center; gap: 10px; margin-top: 8px; flex-wrap: wrap;';
|
|
var modeLabel = document.createElement('span');
|
|
modeLabel.textContent = t('searchMode') + ': ';
|
|
modeLabel.style.cssText = 'color: #888; font-size: 12px;';
|
|
modeRow.appendChild(modeLabel);
|
|
var modes = ['normal', 'wildcard', 'regex'];
|
|
var modeLabels = { 'normal': t('normalMode'), 'wildcard': t('wildcardMode'), 'regex': t('regexMode') };
|
|
modes.forEach(function(mode) {
|
|
var btn = document.createElement('button');
|
|
btn.textContent = modeLabels[mode];
|
|
btn.setAttribute('data-mode', mode);
|
|
btn.style.cssText =
|
|
'padding: 4px 10px;' +
|
|
'font-size: 12px;' +
|
|
'border: 1px solid #4a4a4a;' +
|
|
'border-radius: 4px;' +
|
|
'background: #2a2a2a;' +
|
|
'color: #888;' +
|
|
'cursor: pointer;' +
|
|
'transition: all 0.2s;';
|
|
var updateModeButtons = function() {
|
|
var current = config.getSearchMode();
|
|
modeRow.querySelectorAll('button').forEach(function(b) {
|
|
if (b.getAttribute('data-mode') === current) {
|
|
b.style.background = '#4a9eff';
|
|
b.style.color = '#fff';
|
|
b.style.borderColor = '#4a9eff';
|
|
} else {
|
|
b.style.background = '#2a2a2a';
|
|
b.style.color = '#888';
|
|
b.style.borderColor = '#4a4a4a';
|
|
}
|
|
});
|
|
};
|
|
btn.addEventListener('click', function() {
|
|
config.setSearchMode(mode);
|
|
updateModeButtons();
|
|
var keyword = input.value;
|
|
searchResults = searchNodes(keyword);
|
|
currentResultIndex = searchResults.length > 0 ? 0 : -1;
|
|
updateResultsList();
|
|
});
|
|
modeRow.appendChild(btn);
|
|
});
|
|
modeRow.querySelectorAll('button').forEach(function(btn, idx) {
|
|
if (btn.getAttribute('data-mode') === config.getSearchMode()) {
|
|
btn.style.background = '#4a9eff';
|
|
btn.style.color = '#fff';
|
|
btn.style.borderColor = '#4a9eff';
|
|
}
|
|
});
|
|
inputContainer.appendChild(modeRow);
|
|
dialog.appendChild(inputContainer);
|
|
|
|
|
|
const resultsContainer = document.createElement('div');
|
|
resultsContainer.id = 'search-results-container';
|
|
resultsContainer.style.cssText =
|
|
'flex: 1;' +
|
|
'overflow-y: auto;' +
|
|
'overflow-x: hidden;' +
|
|
'max-height: 400px;' +
|
|
'min-height: 150px;' +
|
|
'border: 1px solid #4a4a4a;' +
|
|
'border-radius: 4px;' +
|
|
'background: #1a1a1a;' +
|
|
'scrollbar-width: thin;' +
|
|
'scrollbar-color: #5a5a5a #2a2a2a;';
|
|
resultsContainer.style.webkitOverflowScrolling = 'touch';
|
|
if (!document.getElementById('mechababy-search-scrollbar-style')) {
|
|
var scrollbarStyle = document.createElement('style');
|
|
scrollbarStyle.id = 'mechababy-search-scrollbar-style';
|
|
scrollbarStyle.textContent =
|
|
'#search-results-container::-webkit-scrollbar { width: 10px; }' +
|
|
'#search-results-container::-webkit-scrollbar-track { background: #2a2a2a; border-radius: 4px; }' +
|
|
'#search-results-container::-webkit-scrollbar-thumb { background: #5a5a5a; border-radius: 5px; }' +
|
|
'#search-results-container::-webkit-scrollbar-thumb:hover { background: #6a6a6a; }';
|
|
document.head.appendChild(scrollbarStyle);
|
|
}
|
|
dialog.appendChild(resultsContainer);
|
|
|
|
|
|
const info = document.createElement('div');
|
|
info.id = 'search-info';
|
|
info.style.cssText =
|
|
'margin-top: 10px;' +
|
|
'font-size: 12px;' +
|
|
'color: #888;' +
|
|
'text-align: center;';
|
|
dialog.appendChild(info);
|
|
|
|
|
|
const footer = document.createElement('div');
|
|
footer.style.cssText = 'display: flex; justify-content: flex-end; margin-top: 10px;';
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.textContent = t('closeButton');
|
|
closeBtn.style.cssText =
|
|
'padding: 8px 16px;' +
|
|
'background: #4a4a4a;' +
|
|
'border: none;' +
|
|
'border-radius: 4px;' +
|
|
'color: #e0e0e0;' +
|
|
'cursor: pointer;' +
|
|
'font-size: 14px;';
|
|
closeBtn.addEventListener('click', closeDialog);
|
|
closeBtn.addEventListener('mouseenter', function() {
|
|
closeBtn.style.background = '#5a5a5a';
|
|
});
|
|
closeBtn.addEventListener('mouseleave', function() {
|
|
closeBtn.style.background = '#4a4a4a';
|
|
});
|
|
footer.appendChild(closeBtn);
|
|
dialog.appendChild(footer);
|
|
|
|
|
|
function updateResultsList() {
|
|
const container = resultsContainer;
|
|
container.innerHTML = '';
|
|
|
|
if (searchResults.length === 0) {
|
|
const emptyMsg = document.createElement('div');
|
|
emptyMsg.textContent = t('noResults');
|
|
emptyMsg.style.cssText =
|
|
'padding: 20px;' +
|
|
'text-align: center;' +
|
|
'color: #888;';
|
|
container.appendChild(emptyMsg);
|
|
info.textContent = '';
|
|
return;
|
|
}
|
|
|
|
var totalMatches = 0;
|
|
for (var i = 0; i < searchResults.length; i++) {
|
|
totalMatches += searchResults[i].matchCount;
|
|
}
|
|
info.textContent = t('foundNodes', searchResults.length, totalMatches);
|
|
|
|
for (var idx = 0; idx < searchResults.length; idx++) {
|
|
(function(index) {
|
|
var result = searchResults[index];
|
|
var item = document.createElement('div');
|
|
item.setAttribute('data-result-index', index);
|
|
item.style.cssText = 'padding: 12px; border-bottom: 1px solid #3a3a3a; cursor: pointer; background: ' + (index === currentResultIndex ? '#3a3a3a' : 'transparent') + '; transition: background 0.2s;';
|
|
item.addEventListener('mouseenter', function() {
|
|
if (index !== currentResultIndex) {
|
|
item.style.background = '#333';
|
|
}
|
|
});
|
|
item.addEventListener('mouseleave', function() {
|
|
if (index !== currentResultIndex) {
|
|
item.style.background = 'transparent';
|
|
}
|
|
});
|
|
item.addEventListener('click', function() {
|
|
currentResultIndex = index;
|
|
updateResultsList();
|
|
if (result.hasError) {
|
|
|
|
if (result.node) {
|
|
try {
|
|
jumpToNode(result.node);
|
|
} catch (e) {
|
|
showToast(t('nodeMayNotLoaded') + ': "' + result.nodeTitle + '"');
|
|
}
|
|
}
|
|
} else {
|
|
|
|
jumpToNode(result.node);
|
|
if (!nodeSearchState.isPinned) {
|
|
closeDialog();
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
var title = document.createElement('div');
|
|
var statusBadge = result.hasError ? ' [' + t('errorNode') + ']' : '';
|
|
title.textContent = result.nodeTitle + statusBadge + ' (' + t('matches', result.matchCount) + ')';
|
|
title.style.cssText = 'font-weight: bold; color: ' + (result.hasError ? '#ff4a4a' : '#4a9eff') + '; margin-bottom: 5px; font-size: 14px;';
|
|
item.appendChild(title);
|
|
|
|
|
|
var type = document.createElement('div');
|
|
var typeText = t('typeLabel') + result.nodeType;
|
|
if (result.hasError) {
|
|
typeText += t('nodeMayNotLoadedHint');
|
|
}
|
|
type.textContent = typeText;
|
|
type.style.cssText = 'color: ' + (result.hasError ? '#ff8888' : '#888') + '; font-size: 12px; margin-bottom: 8px;';
|
|
item.appendChild(type);
|
|
|
|
|
|
var matchesList = document.createElement('div');
|
|
var displayMatches = result.matches.slice(0, 3);
|
|
for (var j = 0; j < displayMatches.length; j++) {
|
|
var match = displayMatches[j];
|
|
var matchItem = document.createElement('div');
|
|
matchItem.textContent = ' • ' + match.display;
|
|
matchItem.style.cssText = 'color: #aaa; font-size: 12px; margin-left: 10px; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;';
|
|
matchesList.appendChild(matchItem);
|
|
}
|
|
if (result.matches.length > 3) {
|
|
var more = document.createElement('div');
|
|
more.textContent = ' ' + t('moreMatches', result.matches.length - 3);
|
|
more.style.cssText = 'color: #666; font-size: 11px; margin-left: 10px; font-style: italic;';
|
|
matchesList.appendChild(more);
|
|
}
|
|
item.appendChild(matchesList);
|
|
|
|
container.appendChild(item);
|
|
})(idx);
|
|
}
|
|
|
|
|
|
if (currentResultIndex >= 0 && currentResultIndex < searchResults.length) {
|
|
setTimeout(function() {
|
|
var selectedItem = container.querySelector('[data-result-index="' + currentResultIndex + '"]');
|
|
if (selectedItem) {
|
|
selectedItem.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'nearest'
|
|
});
|
|
}
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
function closeDialog() {
|
|
if (dialog.parentNode) {
|
|
dialog.parentNode.removeChild(dialog);
|
|
}
|
|
searchDialog = null;
|
|
searchResults = [];
|
|
currentResultIndex = -1;
|
|
}
|
|
|
|
|
|
dialog.addEventListener('click', function(e) {
|
|
if (e.target === dialog) {
|
|
closeDialog();
|
|
}
|
|
});
|
|
|
|
|
|
updateResultsList();
|
|
|
|
return dialog;
|
|
}
|
|
|
|
|
|
function openSearchDialog() {
|
|
if (!searchDialog) {
|
|
searchDialog = createSearchDialog();
|
|
document.body.appendChild(searchDialog);
|
|
} else {
|
|
|
|
if (searchDialog.style.display === 'none') {
|
|
searchDialog.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
|
|
setTimeout(function() {
|
|
var input = searchDialog.querySelector('input');
|
|
if (input) {
|
|
input.focus();
|
|
input.select();
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
|
|
nodeSearchState.openSearchDialog = openSearchDialog;
|
|
|
|
|
|
function checkShortcutMatch(e, shortcut) {
|
|
if (!shortcut) return false;
|
|
|
|
var ctrlMatch = shortcut.ctrl ? (e.ctrlKey || e.metaKey) : (!e.ctrlKey && !e.metaKey);
|
|
var altMatch = shortcut.alt ? e.altKey : !e.altKey;
|
|
var shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey;
|
|
var metaMatch = shortcut.meta ? e.metaKey : !e.metaKey;
|
|
var keyMatch = shortcut.key && (e.key === shortcut.key || e.key === shortcut.key.toLowerCase() || e.key === shortcut.key.toUpperCase());
|
|
|
|
return ctrlMatch && altMatch && shiftMatch && metaMatch && keyMatch;
|
|
}
|
|
|
|
|
|
function bindKeyboardShortcut() {
|
|
|
|
if (nodeSearchState.currentHandler) {
|
|
document.removeEventListener('keydown', nodeSearchState.currentHandler, true);
|
|
window.removeEventListener('keydown', nodeSearchState.currentHandler, true);
|
|
if (app.canvas && app.canvas.canvas) {
|
|
app.canvas.canvas.removeEventListener('keydown', nodeSearchState.currentHandler, true);
|
|
}
|
|
}
|
|
|
|
var shortcut = config.getShortcut();
|
|
|
|
var handler = function(e) {
|
|
|
|
if (checkShortcutMatch(e, shortcut)) {
|
|
|
|
var activeElement = document.activeElement;
|
|
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
|
|
|
|
if (searchDialog && searchDialog.contains(activeElement)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
openSearchDialog();
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
nodeSearchState.currentHandler = handler;
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handler, true);
|
|
|
|
|
|
window.addEventListener('keydown', handler, true);
|
|
|
|
|
|
if (app.canvas) {
|
|
if (app.canvas.canvas) {
|
|
app.canvas.canvas.addEventListener('keydown', handler, true);
|
|
}
|
|
|
|
setTimeout(function() {
|
|
if (app.canvas && app.canvas.canvas) {
|
|
app.canvas.canvas.addEventListener('keydown', handler, true);
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
keyboardHandlerBound = true;
|
|
}
|
|
|
|
|
|
function createSettingsDialog() {
|
|
if (nodeSearchState.settingsDialog) {
|
|
return nodeSearchState.settingsDialog;
|
|
}
|
|
|
|
var dialog = document.createElement('div');
|
|
dialog.id = 'mechababy-node-search-settings';
|
|
dialog.style.cssText =
|
|
'position: fixed;' +
|
|
'top: 50%;' +
|
|
'left: 50%;' +
|
|
'transform: translate(-50%, -50%);' +
|
|
'background: #2a2a2a;' +
|
|
'border: 2px solid #4a4a4a;' +
|
|
'border-radius: 8px;' +
|
|
'padding: 20px;' +
|
|
'z-index: 10001;' +
|
|
'min-width: 400px;' +
|
|
'max-width: 500px;' +
|
|
'box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);' +
|
|
"font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif;";
|
|
|
|
|
|
var title = document.createElement('div');
|
|
title.textContent = '⚙️ ' + t('settings');
|
|
title.style.cssText =
|
|
'font-size: 18px;' +
|
|
'font-weight: bold;' +
|
|
'color: #e0e0e0;' +
|
|
'margin-bottom: 20px;' +
|
|
'border-bottom: 1px solid #4a4a4a;' +
|
|
'padding-bottom: 10px;';
|
|
dialog.appendChild(title);
|
|
|
|
|
|
var shortcutSection = document.createElement('div');
|
|
shortcutSection.style.cssText = 'margin-bottom: 20px;';
|
|
|
|
var shortcutLabel = document.createElement('div');
|
|
shortcutLabel.textContent = t('shortcut') + ':';
|
|
shortcutLabel.style.cssText = 'color: #e0e0e0; margin-bottom: 8px; font-size: 14px;';
|
|
shortcutSection.appendChild(shortcutLabel);
|
|
|
|
var shortcutDisplay = document.createElement('div');
|
|
var currentShortcut = config.getShortcut();
|
|
shortcutDisplay.textContent = t('currentShortcut') + config.formatShortcut(currentShortcut);
|
|
shortcutDisplay.style.cssText = 'color: #4a9eff; margin-bottom: 10px; font-size: 13px; padding: 8px; background: #1a1a1a; border-radius: 4px;';
|
|
shortcutSection.appendChild(shortcutDisplay);
|
|
|
|
var shortcutInput = document.createElement('input');
|
|
shortcutInput.type = 'text';
|
|
shortcutInput.placeholder = t('pressKey');
|
|
shortcutInput.readOnly = true;
|
|
shortcutInput.style.cssText =
|
|
'width: 100%;' +
|
|
'padding: 10px;' +
|
|
'background: #1a1a1a;' +
|
|
'border: 1px solid #4a4a4a;' +
|
|
'border-radius: 4px;' +
|
|
'color: #e0e0e0;' +
|
|
'font-size: 14px;' +
|
|
'box-sizing: border-box;';
|
|
|
|
var capturing = false;
|
|
var capturedShortcut = null;
|
|
|
|
shortcutInput.addEventListener('focus', function() {
|
|
if (!capturing) {
|
|
capturing = true;
|
|
shortcutInput.placeholder = t('pressKey');
|
|
shortcutInput.value = '';
|
|
capturedShortcut = null;
|
|
}
|
|
});
|
|
|
|
shortcutInput.addEventListener('keydown', function(e) {
|
|
if (!capturing) return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
|
|
if (e.key === 'Tab' || e.key === 'Escape' || e.key === 'Enter') {
|
|
return;
|
|
}
|
|
|
|
|
|
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
shortcutInput.value = t('invalidShortcut');
|
|
return;
|
|
}
|
|
|
|
var shortcut = {
|
|
ctrl: e.ctrlKey || e.metaKey,
|
|
alt: e.altKey,
|
|
shift: e.shiftKey,
|
|
meta: e.metaKey,
|
|
key: e.key
|
|
};
|
|
|
|
capturedShortcut = shortcut;
|
|
shortcutInput.value = config.formatShortcut(shortcut);
|
|
});
|
|
|
|
shortcutSection.appendChild(shortcutInput);
|
|
dialog.appendChild(shortcutSection);
|
|
|
|
|
|
var buttonContainer = document.createElement('div');
|
|
buttonContainer.style.cssText = 'display: flex; gap: 10px; justify-content: flex-end;';
|
|
|
|
var saveBtn = document.createElement('button');
|
|
saveBtn.textContent = t('save');
|
|
saveBtn.style.cssText =
|
|
'padding: 8px 16px;' +
|
|
'background: #4a9eff;' +
|
|
'border: none;' +
|
|
'border-radius: 4px;' +
|
|
'color: #fff;' +
|
|
'cursor: pointer;' +
|
|
'font-size: 14px;';
|
|
saveBtn.addEventListener('click', function() {
|
|
if (capturedShortcut) {
|
|
|
|
if (!capturedShortcut.key || (!capturedShortcut.ctrl && !capturedShortcut.meta && !capturedShortcut.alt)) {
|
|
showToast(t('invalidShortcut'));
|
|
return;
|
|
}
|
|
config.setShortcut(capturedShortcut);
|
|
bindKeyboardShortcut();
|
|
showToast(t('shortcutSaved'));
|
|
closeSettingsDialog();
|
|
} else {
|
|
closeSettingsDialog();
|
|
}
|
|
});
|
|
|
|
var cancelBtn = document.createElement('button');
|
|
cancelBtn.textContent = t('cancel');
|
|
cancelBtn.style.cssText =
|
|
'padding: 8px 16px;' +
|
|
'background: #4a4a4a;' +
|
|
'border: none;' +
|
|
'border-radius: 4px;' +
|
|
'color: #e0e0e0;' +
|
|
'cursor: pointer;' +
|
|
'font-size: 14px;';
|
|
cancelBtn.addEventListener('click', closeSettingsDialog);
|
|
|
|
buttonContainer.appendChild(cancelBtn);
|
|
buttonContainer.appendChild(saveBtn);
|
|
dialog.appendChild(buttonContainer);
|
|
|
|
function closeSettingsDialog() {
|
|
if (dialog.parentNode) {
|
|
dialog.parentNode.removeChild(dialog);
|
|
}
|
|
nodeSearchState.settingsDialog = null;
|
|
capturing = false;
|
|
capturedShortcut = null;
|
|
}
|
|
|
|
|
|
dialog.addEventListener('click', function(e) {
|
|
if (e.target === dialog) {
|
|
closeSettingsDialog();
|
|
}
|
|
});
|
|
|
|
nodeSearchState.settingsDialog = dialog;
|
|
return dialog;
|
|
}
|
|
|
|
|
|
function openSettingsDialog() {
|
|
if (!nodeSearchState.settingsDialog) {
|
|
nodeSearchState.settingsDialog = createSettingsDialog();
|
|
document.body.appendChild(nodeSearchState.settingsDialog);
|
|
}
|
|
|
|
|
|
setTimeout(function() {
|
|
var input = nodeSearchState.settingsDialog.querySelector('input');
|
|
if (input) {
|
|
input.focus();
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
|
|
nodeSearchState.openSettingsDialog = openSettingsDialog;
|
|
|
|
|
|
bindKeyboardShortcut();
|
|
|
|
|
|
setTimeout(function() {
|
|
bindKeyboardShortcut();
|
|
}, 1000);
|
|
|
|
var currentShortcut = config.formatShortcut();
|
|
console.log("[MechaBaby NodeSearch] 扩展已加载 - 使用 " + currentShortcut + " 打开搜索");
|
|
console.log("[MechaBaby NodeSearch] 当前语言: " + config.getLanguage());
|
|
}
|
|
});
|
|
|