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()); } });