您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Converts NotebookLM mindmap SVG to hierarchical Freemind (.mm). Auto-expands nodes. Matches path ends to node EDGE MIDPOINTS. REQUIRES ACCURATE SELECTORS. Check console.
// ==UserScript== // @name SVG Mindmap to Freemind Converter (NotebookLM - Hierarchical) // @namespace http://tampermonkey.net/ // @version 2.4 // @description Converts NotebookLM mindmap SVG to hierarchical Freemind (.mm). Auto-expands nodes. Matches path ends to node EDGE MIDPOINTS. REQUIRES ACCURATE SELECTORS. Check console. // @author mike868(小红书:迈克八六八) // @match https://notebooklm.google.com/notebook/* // @grant GM_download // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Configuration --- // ====> !!! CRITICAL: YOU **MUST** INSPECT THE LIVE NOTEBOOKLM SVG AND ADJUST THESE !!! <==== const svgSelector = 'div.mindmap > svg'; // Selector for the main SVG element. const nodeGroupSelector = 'g[transform]'; // Selector for the <g> containing all nodes/paths. Try 'g' if unsure. const nodeSelector = 'g.node'; // Selector for individual node <g> elements. const rectSelector = 'rect'; // Selector *within* a node <g> for the main background rectangle. const textSelector = 'text.node-name'; // Selector *within* a node <g> for the primary text. const textFallbackSelector = 'text'; // Fallback selector for text. const linkSelector = 'path.link'; // Selector for connection <path> elements. const expandCircleSelector = 'circle'; // Selector for expandable node circles. const expandSymbolSelector = 'text.expand-symbol'; // Selector for expand/collapse symbols (> or <). const NODE_MATCH_TOLERANCE_PX = 85; // How close (pixels) path endpoint needs to be to node *edge midpoint*. ADJUST IF NEEDED. const EXPAND_DELAY_MS = 300; // Delay between expanding nodes (to prevent overwhelming the browser). const MAX_EXPAND_ATTEMPTS = 3; // Maximum number of attempts to expand all nodes. // ========================================================================================== const buttonText = 'Convert MindMap to .mm (Hierarchy v2.4 Edge Match)'; const buttonId = 'svg-to-mm-converter-button-v2-4-hierarchical'; const expandButtonText = 'Expand All Nodes'; const expandButtonId = 'expand-all-nodes-button'; const NODE_MATCH_TOLERANCE_SQ = NODE_MATCH_TOLERANCE_PX * NODE_MATCH_TOLERANCE_PX; console.log("SVG to MM Script v2.4 (Edge Match + Auto-Expand) Loaded. Waiting for page content..."); // --- Helper Functions --- function escapeXml(unsafe) { /* ... same ... */ if(typeof unsafe !== 'string') return ''; return unsafe.replace(/[<>&"']/g, c => ({'<':'<','>':'>','&':'&','"':'"',"'":'''})[c]); } function getTranslateXY(element) { /* ... same ... */ if(!element || typeof element.getAttribute !== 'function') return null; try { const transform = element.getAttribute('transform'); if(transform) { const match = transform.match(/translate\(\s*([^,\s]+)\s*,\s*([^,\s\)]+)\s*\)/); if(match) return { x: parseFloat(match[1]), y: parseFloat(match[2]) }; } } catch (e) { console.warn("Error parsing translate:", e, element); } return null; } function parsePathEndpoints(d) { /* ... same ... */ if(!d) return null; try { const moveMatch = d.match(/M\s*(-?[\d\.]+)\s*[,\s]\s*(-?[\d\.]+)/i); const endCoordMatch = d.match(/(-?[\d\.]+)\s*[,\s]\s*(-?[\d\.]+)\s*Z?$/i); if(moveMatch && endCoordMatch) return { startX: parseFloat(moveMatch[1]), startY: parseFloat(moveMatch[2]), endX: parseFloat(endCoordMatch[1]), endY: parseFloat(endCoordMatch[2]) }; else { console.warn(`Path parse failed primary regex:`, d); /* fallback omitted for brevity but could be added back */ } } catch (e) { console.error("Path parse error:", e, d); } return null; } function distanceSq(p1, p2) { /* ... same ... */ if (!p1 || !p2) return Infinity; const dx = p1.x - p2.x; const dy = p1.y - p2.y; return dx * dx + dy * dy; } /** * Finds the node whose specified edge midpoint(s) are closest to a given point. * @param {{x: number, y: number}} point The target point (e.g., path endpoint). * @param {Array} nodesWithEdges Array of node objects { id, leftEdgeMid?: {x, y}, rightEdgeMid?: {x, y}, ... }. * @param {number} toleranceSq Max squared distance allowed. * @param {Array<string>} edgeKeys Array of keys for edge midpoints to check (e.g., ['leftEdgeMid', 'rightEdgeMid']). * @returns {object | null} The closest node object or null. */ function findClosestNodeByEdge(point, nodesWithEdges, toleranceSq, edgeKeys) { let closestNode = null; let minDistSq = Infinity; let closestEdgeKey = null; // For debugging if (!point || !edgeKeys || edgeKeys.length === 0) { console.warn("findClosestNodeByEdge: Invalid point or edgeKeys.", {point, edgeKeys}); return null; } nodesWithEdges.forEach(node => { edgeKeys.forEach(key => { const edgeMidpoint = node[key]; if (edgeMidpoint) { // Check if this specific edge point exists for the node const distSq = distanceSq(point, edgeMidpoint); if (distSq < minDistSq) { minDistSq = distSq; closestNode = node; closestEdgeKey = key; // Store which edge matched best } } }); }); if (closestNode && minDistSq <= toleranceSq) { // console.log(`-> Matched point (${point.x.toFixed(1)}, ${point.y.toFixed(1)}) to node "${closestNode.text}" via edge "${closestEdgeKey}" (Dist: ${Math.sqrt(minDistSq).toFixed(1)}px)`); return closestNode; } else { // console.warn(`-> No node edge (${edgeKeys.join('/')}) found close enough to point (${point.x.toFixed(1)}, ${point.y.toFixed(1)}). ` + // `Closest edge dist: ${Math.sqrt(minDistSq).toFixed(1)}px`); return null; } } function getNodeText(svgNodeElement) { /* ... same as v2.2 ... */ if(!svgNodeElement || typeof svgNodeElement.querySelector !== 'function') return 'Invalid Element'; let nodeText = 'Untitled'; try { let textElement = svgNodeElement.querySelector(`:scope > ${textSelector}`); if(!textElement) textElement = svgNodeElement.querySelector(`:scope > ${textFallbackSelector}`); if(textElement && textElement.textContent){ nodeText = textElement.textContent.trim(); const tspans = textElement.querySelectorAll(':scope > tspan'); if(tspans.length > 0) nodeText = Array.from(tspans).map(tspan => tspan.textContent.trim()).join(' ').trim(); nodeText = nodeText.replace(/\s+/g, ' '); } else { console.warn(`getNodeText: No text found:`, svgNodeElement); nodeText = (svgNodeElement.textContent || '').replace(/\s+/g, ' ').trim() || 'Untitled Fallback'; } } catch (e) { console.error("Error in getNodeText:", svgNodeElement, e); nodeText = "Error Reading Text"; } return nodeText || "Untitled Error"; } /** * Attempts to click on an SVG element using various methods * @param {Element} element The element to click * @param {string} description Description for logging * @returns {boolean} Whether any click method succeeded */ function attemptClickOnElement(element, description) { if (!element) return false; let clickSuccess = false; // Method 1: dispatchEvent with MouseEvent try { const clickEvent = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); element.dispatchEvent(clickEvent); console.log(`Method 1 (MouseEvent) attempted on ${description}`); clickSuccess = true; } catch (e) { console.warn(`Method 1 (MouseEvent) failed on ${description}: ${e.message}`); } // Method 2: createEvent (older browsers) if (!clickSuccess) { try { const clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent('click', true, true); element.dispatchEvent(clickEvent); console.log(`Method 2 (createEvent) attempted on ${description}`); clickSuccess = true; } catch (e) { console.warn(`Method 2 (createEvent) failed on ${description}: ${e.message}`); } } // Method 3: Trigger click via parent HTML element if available if (!clickSuccess && element.parentElement && !(element instanceof SVGElement)) { try { element.parentElement.click(); console.log(`Method 3 (parent click) attempted on ${description}`); clickSuccess = true; } catch (e) { console.warn(`Method 3 (parent click) failed on ${description}: ${e.message}`); } } return clickSuccess; } /** * Expands all collapsible nodes in the mindmap. * @param {Element} svgElement The SVG element containing the mindmap. * @param {Function} callback Optional callback to run after expansion is complete. */ function expandAllNodes(svgElement, callback) { if (!svgElement) { console.error("expandAllNodes: No SVG element provided."); if (callback) callback(false); return; } console.log("Starting node expansion process..."); // Find all nodes with ">" expand symbol (collapsed nodes) const mainGroup = svgElement.querySelector(nodeGroupSelector) || svgElement; let expandableNodes = Array.from(mainGroup.querySelectorAll(`${nodeSelector} ${expandSymbolSelector}`)) .filter(symbol => symbol.textContent === ">" || symbol.textContent.includes(">")); if (expandableNodes.length === 0) { console.log("No expandable nodes found."); if (callback) callback(true); return; } console.log(`Found ${expandableNodes.length} expandable nodes.`); let expandedCount = 0; let attemptCount = 0; let previousCount = -1; // Function to expand nodes with delay function expandNodesWithDelay() { // If no progress was made in the last attempt and we've tried enough times, stop if (previousCount === expandableNodes.length && attemptCount >= MAX_EXPAND_ATTEMPTS) { console.log(`Stopped expanding after ${attemptCount} attempts. ${expandedCount} nodes expanded. ${expandableNodes.length} nodes still collapsed.`); if (callback) callback(expandedCount > 0); return; } previousCount = expandableNodes.length; attemptCount++; // Find expandable nodes again (as DOM may have changed) expandableNodes = Array.from(mainGroup.querySelectorAll(`${nodeSelector} ${expandSymbolSelector}`)) .filter(symbol => symbol.textContent === ">" || symbol.textContent.includes(">")); if (expandableNodes.length === 0) { console.log(`All nodes expanded successfully after ${attemptCount} attempts.`); if (callback) callback(true); return; } console.log(`Attempt ${attemptCount}: Found ${expandableNodes.length} nodes to expand.`); // Get the parent node of the expand symbol and find its circle const nodeToExpand = expandableNodes[0]; const parentNode = nodeToExpand.closest(nodeSelector); if (parentNode) { const nodeText = getNodeText(parentNode); console.log(`Trying to expand node: "${nodeText}"`); let clickSuccess = false; // Try 1: Click on the circle element const expandCircle = parentNode.querySelector(expandCircleSelector); if (expandCircle) { console.log(`Trying to click circle for node: "${nodeText}"`); clickSuccess = attemptClickOnElement(expandCircle, `circle for "${nodeText}"`); } // Try 2: Click on the expand symbol text if (!clickSuccess) { console.log(`Trying to click expand symbol for node: "${nodeText}"`); clickSuccess = attemptClickOnElement(nodeToExpand, `expand symbol for "${nodeText}"`); } // Try 3: Click on the rect element if (!clickSuccess) { const rect = parentNode.querySelector(rectSelector); if (rect) { console.log(`Trying to click rect for node: "${nodeText}"`); clickSuccess = attemptClickOnElement(rect, `rect for "${nodeText}"`); } } // Try 4: Click on the parent node itself if (!clickSuccess) { console.log(`Trying to click parent node for: "${nodeText}"`); clickSuccess = attemptClickOnElement(parentNode, `parent node for "${nodeText}"`); } if (clickSuccess) { expandedCount++; } else { console.warn(`Failed to expand node "${nodeText}" after all attempts`); } // Continue with next node after a delay setTimeout(expandNodesWithDelay, EXPAND_DELAY_MS); } else { console.warn("Could not find parent node for expand symbol:", nodeToExpand); setTimeout(expandNodesWithDelay, EXPAND_DELAY_MS); } } // Start expanding nodes expandNodesWithDelay(); } function generateMmNodeXml(node, nodeMap, indent, parentNode) { /* ... same logic as v2.2, uses centerPos or pos for POSITION attr ... */ let position = ''; const nodePosForSort = node.centerPos || node.pos; // Prefer center for position attribute too const parentPosForSort = parentNode ? (parentNode.centerPos || parentNode.pos) : null; if (parentPosForSort && nodePosForSort) { const positionBuffer = 10; if (nodePosForSort.x > parentPosForSort.x + positionBuffer) position = 'right'; else if (nodePosForSort.x < parentPosForSort.x - positionBuffer) position = 'left'; } const posAttr = position ? ` POSITION="${position}"` : ''; const nodeIdText = node.text.substring(0, 20).replace(/[^a-zA-Z0-9]/g, '_'); const nodeId = `ID_${node.id || nodeIdText || 'node'}`; let xml = `${indent}<node TEXT="${escapeXml(node.text)}"${posAttr} ID="${nodeId}"`; if (node.children && node.children.length > 0) { xml += `>\n`; const sortedChildren = node.children .map(id => nodeMap.get(id)) .filter(Boolean) .sort((a, b) => ((a.centerPos?.y ?? a.pos?.y ?? 0) - (b.centerPos?.y ?? b.pos?.y ?? 0))); // Sort by Y sortedChildren.forEach(childNode => { xml += generateMmNodeXml(childNode, nodeMap, indent + '\t', node); }); xml += `${indent}</node>\n`; } else { xml += `/>\n`; } return xml; } function convertSvgToHierarchicalFreemind(svgElement) { console.log("convertSvgToHierarchicalFreemind v2.4: Starting conversion..."); if (!svgElement || typeof svgElement.querySelector !== 'function') { /*...*/ return null; } const mainGroup = svgElement.querySelector(nodeGroupSelector); const containerElement = mainGroup || svgElement; console.log("Using container:", containerElement.tagName); const nodeElements = Array.from(containerElement.querySelectorAll(`:scope > ${nodeSelector}`)); const linkElements = Array.from(containerElement.querySelectorAll(`:scope > ${linkSelector}`)); console.log(`Found ${nodeElements.length} nodes, ${linkElements.length} links.`); if (nodeElements.length === 0) { /*...*/ return null; } // 1. Extract Node Info (including Edge Midpoint Calculation) const nodeMap = new Map(); const nodesWithEdges = []; // Holds nodes with successfully calculated edge positions let nodeCounter = 0; nodeElements.forEach((el) => { nodeCounter++; const nodeId = `node_${nodeCounter}`; const nodePos = getTranslateXY(el); const text = getNodeText(el); if (!text || text.startsWith('Untitled') || text.startsWith('Error') || text === 'Invalid Element') { /* skip */ return; } let centerPos = null, leftEdgeMid = null, rightEdgeMid = null; let nodeRectElem = null; try { nodeRectElem = el.querySelector(`:scope > ${rectSelector}`); if (nodeRectElem && nodePos) { const rx = parseFloat(nodeRectElem.getAttribute('x') || '0'); const ry = parseFloat(nodeRectElem.getAttribute('y') || '0'); const rwidth = parseFloat(nodeRectElem.getAttribute('width') || '0'); const rheight = parseFloat(nodeRectElem.getAttribute('height') || '0'); if (rwidth > 0 && rheight > 0) { // Calculate center (still useful for sorting/position attr) centerPos = { x: nodePos.x + rx + rwidth / 2, y: nodePos.y + ry + rheight / 2 }; // Calculate edge midpoints (absolute coords) leftEdgeMid = { x: nodePos.x + rx, y: nodePos.y + ry + rheight / 2 }; rightEdgeMid = { x: nodePos.x + rx + rwidth, y: nodePos.y + ry + rheight / 2 }; // console.log(`Node "${text}": translate=(${nodePos.x.toFixed(0)}, ${nodePos.y.toFixed(0)}), L-Edge=(${leftEdgeMid.x.toFixed(0)}, ${leftEdgeMid.y.toFixed(0)}), R-Edge=(${rightEdgeMid.x.toFixed(0)}, ${rightEdgeMid.y.toFixed(0)})`); } else { console.warn(`Node "${text}": Rect width/height invalid.`); } } else if (nodePos) { console.warn(`Node "${text}": Rect not found or node has no pos. Cannot calc edges.`); } else { console.warn(`Node "${text}": No position.`); } } catch(e) { console.error(`Error processing rect for node "${text}":`, e, el); } const nodeData = { id: nodeId, text: text, pos: nodePos, centerPos: centerPos, leftEdgeMid: leftEdgeMid, rightEdgeMid: rightEdgeMid, // Store edge points parentId: null, children: [], }; nodeMap.set(nodeId, nodeData); // Add to list for matching only if edge points were calculable if (leftEdgeMid && rightEdgeMid) { nodesWithEdges.push(nodeData); } }); console.log(`Processed ${nodeMap.size} nodes. ${nodesWithEdges.length} nodes have edge midpoints calculated for linking.`); if (nodesWithEdges.length < 2 && nodeMap.size >= 2) { console.warn("Not enough nodes with calculable edges. Linking likely impossible."); } // 2. Extract Links and Match Parent/Child (using node edge midpoints) let linksEstablished = 0; let pathsParsed = 0; linkElements.forEach((linkEl, index) => { const d = linkEl.getAttribute('d'); if (!d) { return; } const endpoints = parsePathEndpoints(d); if (endpoints) { pathsParsed++; const startPoint = { x: endpoints.startX, y: endpoints.startY }; const endPoint = { x: endpoints.endX, y: endpoints.endY }; // console.log(`Path ${index+1}: Start=(${startPoint.x.toFixed(1)}, ${startPoint.y.toFixed(1)}), End=(${endPoint.x.toFixed(1)}, ${endPoint.y.toFixed(1)})`); // Try to find parent by matching startPoint to a node's edge // Prioritize right edge, then left edge let parentNode = findClosestNodeByEdge(startPoint, nodesWithEdges, NODE_MATCH_TOLERANCE_SQ, ['rightEdgeMid']); if (!parentNode) { parentNode = findClosestNodeByEdge(startPoint, nodesWithEdges, NODE_MATCH_TOLERANCE_SQ, ['leftEdgeMid']); } // Try to find child by matching endPoint to a node's edge // Prioritize left edge, then right edge let childNode = findClosestNodeByEdge(endPoint, nodesWithEdges, NODE_MATCH_TOLERANCE_SQ, ['leftEdgeMid']); if (!childNode) { childNode = findClosestNodeByEdge(endPoint, nodesWithEdges, NODE_MATCH_TOLERANCE_SQ, ['rightEdgeMid']); } if (parentNode && childNode && parentNode.id !== childNode.id) { const childNodeData = nodeMap.get(childNode.id); const parentNodeData = nodeMap.get(parentNode.id); if (!childNodeData || !parentNodeData) { console.error(`Internal Map Error`); return; } if (childNodeData.parentId === null) { childNodeData.parentId = parentNodeData.id; if (!parentNodeData.children) parentNodeData.children = []; if (!parentNodeData.children.includes(childNodeData.id)){ parentNodeData.children.push(childNodeData.id); linksEstablished++; console.log(`Link ${linksEstablished}: "${parentNodeData.text}" -> "${childNodeData.text}" (via path ${index+1})`); } } else if (childNodeData.parentId !== parentNodeData.id) { console.warn(`Child "${childNodeData.text}" already has parent. Tried assigning "${parentNodeData.text}". Path ${index+1}`); } } else { // Log failure reason let reason = ""; if (!parentNode) reason += ` Could not match path start (${startPoint.x.toFixed(0)},${startPoint.y.toFixed(0)}) to any node edge (L/R).`; if (!childNode) reason += ` Could not match path end (${endPoint.x.toFixed(0)},${endPoint.y.toFixed(0)}) to any node edge (L/R).`; if (parentNode && childNode && parentNode.id === childNode.id) reason += ` Path endpoints matched same node ("${parentNode.text}").`; console.warn(`Failed link for path ${index + 1}.${reason}`); } } else { console.warn(`Skipping link ${index + 1} - bad 'd'`); } }); console.log(`Parsed ${pathsParsed}/${linkElements.length} paths. Established ${linksEstablished} links via edge matching.`); if (nodeMap.size > 1 && linksEstablished === 0 && linkElements.length > 0) { console.error("CRITICAL: Edge linking failed. Check console warnings. Ensure rect selector is correct, try increasing tolerance drastically (e.g., 150), or SVG structure is unexpected."); alert("Conversion Warning (v2.4): Failed to link nodes using edges. Check console (F12). Verify selectors, try higher tolerance."); } // 3. Find Root Node(s) const rootNodesData = []; /* ... same logic as v2.2 ... */ nodeMap.forEach(node => { if(node.parentId === null) rootNodesData.push(node); }); console.log(`Found ${rootNodesData.length} potential root(s).`); let actualRootNode = null; /* ... same root selection logic as v2.2 ... */ let rootText = "MindmapRoot"; if(rootNodesData.length === 0 && nodeMap.size > 0) { console.error("No root found!"); /* fallback... */ if(nodesWithEdges.length > 0) { nodesWithEdges.sort((a,b) => (a.centerPos?.x ?? a.pos?.x ?? Infinity) - (b.centerPos?.x ?? b.pos?.x ?? Infinity)); actualRootNode = nodesWithEdges[0]; console.warn(`Fallback root: "${actualRootNode?.text}"`); } else { actualRootNode = nodeMap.values().next().value; } if(!actualRootNode) { alert("Crit Err: No root"); return null; } } else if (rootNodesData.length > 1) { console.warn("Multiple roots:", rootNodesData.map(n=>n.text)); rootNodesData.sort((a,b)=>{ const ax = a.centerPos?.x ?? a.pos?.x ?? Infinity; const bx = b.centerPos?.x ?? b.pos?.x ?? Infinity; if (ax !== bx) return ax - bx; const ay = a.centerPos?.y ?? a.pos?.y ?? Infinity; const by = b.centerPos?.y ?? b.pos?.y ?? Infinity; return ay - by; }); actualRootNode = rootNodesData[0]; console.warn(`Selected root: "${actualRootNode?.text}"`); } else { actualRootNode = rootNodesData[0]; console.log(`Single root: "${actualRootNode?.text}"`); } if(!actualRootNode) { console.error("No root determined."); return null; } rootText = actualRootNode.text; // 4. Generate Hierarchical XML console.log(`Generating XML from root: "${rootText}"`); /* ... same as v2.2 ... */ let mindmapXmlBody = ""; try { mindmapXmlBody = generateMmNodeXml(actualRootNode, nodeMap, '\t', null); } catch (e) { console.error("XML Gen Error:", e); return null; } if (!mindmapXmlBody) { console.error("XML Empty"); return null; } // --- Final XML --- const freemindXml = `<map version="1.0.1">\n` + /* ... same header comments as v2.2 + version bump ... */ `<!-- Mind map converted from SVG by Userscript v2.4 (Hierarchical/Edge Match with Auto-Expand) -->\n` + `<!-- Selectors: svg='${svgSelector}', group='${nodeGroupSelector}', node='${nodeSelector}', rect='${rectSelector}', link='${linkSelector}', text='${textSelector}' -->\n`+ `<!-- Tolerance: ${NODE_MATCH_TOLERANCE_PX}px -->\n`+ mindmapXmlBody + `</map>`; console.log("v2.3 Conversion process completed."); return { mmContent: freemindXml, rootText: rootText }; } // --- Download Function --- (Identical) function downloadMMFile(filename, content) { /* ... same ... */ console.log(`Download: ${filename}`); try { GM_download({ url: `data:application/xml;charset=utf-8,${encodeURIComponent(content)}`, name: filename, saveAs: true, onerror: (err) => { console.error("GM_download error:", err); alert(`DL fail: ${err.error?.message || 'Unknown'}`); }, onload: () => { console.log(`DL initiated: ${filename}`); } }); } catch (e) { console.error("DL Call Error:", e); alert("DL Error: Tampermonkey/Permissions?"); } } // --- Button Creation and Event Handling --- function createConversionButton() { if(document.getElementById(buttonId)) return; const button = document.createElement('button'); button.id = buttonId; button.textContent = buttonText; /* Style */ button.style.position = 'fixed'; button.style.bottom = '60px'; button.style.right = '20px'; button.style.zIndex = '10003'; /* Higher z */ button.style.padding = '10px 15px'; button.style.backgroundColor = '#fbbc05'; /* Yellow */ button.style.color = 'black'; button.style.border = 'none'; button.style.borderRadius = '4px'; button.style.cursor = 'pointer'; button.style.fontSize = '14px'; button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)'; button.style.fontFamily = 'Roboto, Arial, sans-serif'; /* Event */ button.addEventListener('click', () => { console.log(`Button "${buttonText}" clicked.`); button.textContent = "Processing..."; // Use these properties instead of disabled attribute to avoid focus issues button.style.pointerEvents = 'none'; button.style.opacity = '0.7'; button.style.cursor = 'default'; setTimeout(() => { try { const svg = document.querySelector(svgSelector); if(!svg) { throw new Error("SVG missing!"); } console.log("SVG found:", svg); const result = convertSvgToHierarchicalFreemind(svg); if(result?.mmContent) { let fn = "mindmap_hierarchy.mm"; if(result.rootText && !result.rootText.startsWith('Untitled')) { fn = result.rootText.replace(/[<>:"/\\|?*\s\.]+/g, '_').replace(/_+/g, '_').substring(0, 100); fn = (fn || "mindmap") + ".mm"; } console.log(`Success. Root: "${result.rootText}". Download: ${fn}`); downloadMMFile(fn, result.mmContent); button.textContent = "DL Initiated"; } else { alert("Edge Conversion failed (v2.4). Check console (F12). Verify selectors/tolerance."); console.error("v2.4 result null/empty."); button.textContent = "Convert Fail"; } } catch(error) { alert("Error during v2.4 conversion. Check console (F12)."); console.error("v2.4 click error:", error); button.textContent = "Error!"; } finally { setTimeout(() => { button.textContent = buttonText; // Re-enable the button button.style.pointerEvents = ''; button.style.opacity = ''; button.style.cursor = ''; }, 3000); } }, 150); }); document.body.appendChild(button); console.log("v2.4 Edge Match button added."); } // Create a button to expand all nodes function createExpandButton() { if (document.getElementById(expandButtonId)) return; const button = document.createElement('button'); button.id = expandButtonId; button.textContent = expandButtonText; // Style similar to conversion button but different color button.style.position = 'fixed'; button.style.bottom = '110px'; // Position above the conversion button button.style.right = '20px'; button.style.zIndex = '10003'; button.style.padding = '10px 15px'; button.style.backgroundColor = '#4285f4'; // Blue button.style.color = 'white'; button.style.border = 'none'; button.style.borderRadius = '4px'; button.style.cursor = 'pointer'; button.style.fontSize = '14px'; button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)'; button.style.fontFamily = 'Roboto, Arial, sans-serif'; // Event button.addEventListener('click', () => { console.log(`Button "${expandButtonText}" clicked.`); button.textContent = "Expanding..."; // Use these properties instead of disabled attribute to avoid focus issues button.style.pointerEvents = 'none'; button.style.opacity = '0.7'; button.style.cursor = 'default'; setTimeout(() => { try { const svg = document.querySelector(svgSelector); if (!svg) { throw new Error("SVG missing!"); } expandAllNodes(svg, (success) => { if (success) { button.textContent = "Expanded!"; console.log("All nodes expanded successfully."); } else { button.textContent = "Expand Failed"; console.error("Failed to expand all nodes."); } setTimeout(() => { button.textContent = expandButtonText; // Re-enable the button button.style.pointerEvents = ''; button.style.opacity = ''; button.style.cursor = ''; }, 2000); }); } catch (error) { alert("Error during node expansion. Check console (F12)."); console.error("Expand click error:", error); button.textContent = "Error!"; setTimeout(() => { button.textContent = expandButtonText; // Re-enable the button button.style.pointerEvents = ''; button.style.opacity = ''; button.style.cursor = ''; }, 2000); } }, 150); }); document.body.appendChild(button); console.log("Expand All button added."); } // --- Script Execution --- let observer = null; let buttonAdded = false; function initObserver() { console.log("Init Observer v2.4 for:", svgSelector); const target = document.body; const config = { childList: true, subtree: true }; const callback = (mutations, obs) => { const svgExists = document.querySelector(svgSelector); if(svgExists && !buttonAdded) { console.log("SVG detected v2.4"); if(!document.getElementById(buttonId)) { createConversionButton(); createExpandButton(); buttonAdded = true; } } else if (!svgExists && buttonAdded) { console.log("SVG removed v2.4"); buttonAdded = false; const btn = document.getElementById(buttonId); if(btn) btn.remove(); const expandBtn = document.getElementById(expandButtonId); if(expandBtn) expandBtn.remove(); } }; observer = new MutationObserver(callback); observer.observe(target, config); if(document.querySelector(svgSelector) && !buttonAdded) { console.log("SVG already present v2.4"); if(!document.getElementById(buttonId)) { createConversionButton(); createExpandButton(); buttonAdded = true; } } } if(document.readyState === "complete" || document.readyState === "interactive") { initObserver(); } else { window.addEventListener("load", initObserver); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址