// ==UserScript==
// @name NotebookLM → Export to Obsidian Canvas (Auto Title)
// @namespace notebooklm
// @version 1.1
// @description Добавляет кнопку для экспорта mind map в формат Obsidian Canvas (.canvas) и называет файл по названию карты из NotebookLM
// @author You
// @match *://*notebooklm.google.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
function addExportButton() {
if (document.getElementById('obsidianExportBtn')) return;
const btn = document.createElement('button');
btn.id = 'obsidianExportBtn';
btn.textContent = '💾 Скачать для Obsidian';
Object.assign(btn.style, {
position: 'fixed',
top: '16px',
right: '16px',
zIndex: 9999,
padding: '10px 16px',
background: '#4a90e2',
color: '#fff',
border: 'none',
borderRadius: '8px',
fontWeight: 'bold',
cursor: 'pointer',
boxShadow: '0 2px 6px rgba(0,0,0,0.25)'
});
btn.onmouseenter = () => btn.style.background = '#357ad8';
btn.onmouseleave = () => btn.style.background = '#4a90e2';
btn.addEventListener('click', exportToObsidian);
document.body.appendChild(btn);
}
function waitForMindmap() {
if (document.querySelector('g.node')) {
addExportButton();
} else {
setTimeout(waitForMindmap, 1500);
}
}
function exportToObsidian() {
(() => {
// ======= НАСТРОЙКИ =======
const NODE_SELECTOR = 'g.node';
const TEXT_SELECTOR = 'text.node-name';
const RECT_SELECTOR = 'rect';
const PATH_SELECTOR = 'path';
const LINE_SELECTOR = 'line';
const SAMPLES = 60;
const TOL = 40; // чувствительность связей (px)
// =========================
const float = s => parseFloat(s || 0);
const parseTranslate = tr => {
const m = tr && tr.match(/translate\(\s*([-\d.]+)[,\s]+([-\d.]+)\s*\)/);
return m ? { x: +m[1], y: +m[2] } : { x: 0, y: 0 };
};
const pointToRectDist = (px, py, n) => {
const dx = Math.max(n.left - px, 0, px - n.right);
const dy = Math.max(n.top - py, 0, py - n.bottom);
return Math.sqrt(dx * dx + dy * dy);
};
// === 1. Узлы ===
const nodes = [];
document.querySelectorAll(NODE_SELECTOR).forEach((g, i) => {
const { x: tx, y: ty } = parseTranslate(g.getAttribute('transform'));
const rect = g.querySelector(RECT_SELECTOR);
if (!rect) return;
const rx = float(rect.getAttribute('x'));
const ry = float(rect.getAttribute('y'));
const w = float(rect.getAttribute('width'));
const h = float(rect.getAttribute('height'));
const left = tx + rx;
const top = ty + ry;
const right = left + w;
const bottom = top + h;
const text = g.querySelector(TEXT_SELECTOR)?.textContent.trim() || '';
nodes.push({
id: g.id || `n${i}`,
left, right, top, bottom,
x: tx, y: ty, width: w, height: h,
text
});
});
// === 2. Связи ===
const paths = [...document.querySelectorAll(PATH_SELECTOR), ...document.querySelectorAll(LINE_SELECTOR)];
const edges = [];
paths.forEach(path => {
let points = [];
try {
const len = path.getTotalLength();
for (let i = 0; i <= SAMPLES; i++) {
const p = path.getPointAtLength((len * i) / SAMPLES);
points.push({ x: p.x, y: p.y });
}
} catch {
const nums = (path.getAttribute('d') || '').match(/[-+]?\d*\.?\d+/g);
if (nums && nums.length >= 4) {
points = [
{ x: +nums[0], y: +nums[1] },
{ x: +nums[nums.length - 2], y: +nums[nums.length - 1] }
];
}
}
if (!points.length) return;
const start = points[0];
const end = points[points.length - 1];
let from = null, to = null, minF = 1e9, minT = 1e9;
for (const n of nodes) {
const df = pointToRectDist(start.x, start.y, n);
const dt = pointToRectDist(end.x, end.y, n);
if (df < minF) { minF = df; from = n; }
if (dt < minT) { minT = dt; to = n; }
}
if (from && to && from !== to && minF < TOL && minT < TOL) {
edges.push({ fromNode: from.id, toNode: to.id });
}
});
// === 3. Подготовка к Obsidian Canvas ===
const minX = Math.min(...nodes.map(n => n.left));
const minY = Math.min(...nodes.map(n => n.top));
const canvasNodes = nodes.map(n => ({
id: n.id,
x: Math.round((n.left - minX) * 1.2),
y: Math.round((n.top - minY) * 1.2),
width: Math.round(n.width + 20),
height: Math.round(n.height + 20),
type: "text",
text: n.text
}));
const canvasEdges = edges.map(e => ({
id: `${e.fromNode}-${e.toNode}`,
fromNode: e.fromNode,
toNode: e.toNode
}));
const canvasData = { nodes: canvasNodes, edges: canvasEdges };
// === 4. Название карты ===
const titleEl = document.querySelector('.mindmap-title');
let filename = 'mindmap.canvas';
if (titleEl) {
let title = titleEl.textContent.trim();
title = title.replace(/[\\/:*?"<>|]+/g, ''); // очистка от недопустимых символов
filename = `${title}.canvas`;
}
// === 5. Скачивание ===
const blob = new Blob([JSON.stringify(canvasData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
console.log(`✅ Готово! Файл сохранён как: ${filename}`);
console.log(canvasData);
window.mm_canvas = canvasData;
})();
}
waitForMindmap();
})();