我是如何像Obsidian Publish那样在本地集成Graph View的
Obsidian Publish 在笔记中集成了 Graph View,让浏览笔记的时候能够查看与当前笔记关联的知识图谱,帮助进行结构化地理解和思考。
比较遗憾的是Obsidian App原生没有支持该能力——将graph view集成到笔记中进行预览,这篇文章就是介绍一下我是如何实现这个能力的,先看一下效果。
设计思路
local graph是由Obsidian内部的引擎计算实现的,由canvas绘制图形(类似chartjs),我只需要将canvas元素插入到当前笔记的workspace leaf中即可:
- 通过local graph命令绘制canvas图形;
- 将canvas图形插入到当前笔记合适的位置;
- 去除canvas原先的view(window);
一步一坑
1. local graph绘制
直接使用obsidian原生的local graph命令绘制当前笔记的关联图谱的view。
await this.app.commands.executeCommandById("graph:open-local");
在实验过程中发现,如果这样的话执行的话会创建split view,直接影响obsidian的使用体验。
经过权衡,可以通过创建独立的window来解决这个问题:
await this.app.commands.executeCommandById("graph:open-local");
await new Promise((resolve) => setTimeout(resolve, 200));
const graphLeaf = this.app.workspace.getLeavesOfType('localgraph')[0];
this.app.workspace.moveLeafToPopout(graphLeaf);
2. canvas插入当前笔记
这一步骤属于基础的html query操作,通过view-content
找到local graph的元素,然后将local graph的canvas插入到指定元素的位置(当前选择的是view-header
元素)。
const localgraph = graphLeaf.view.containerEl.getElementsByClassName('view-content')[0];
const noteHeader = fileLeaf.containerEl.getElementsByClassName('view-header')[0];
noteHeader.parentElement.insertAfter(localgraph, noteHeader);
在实验过程中发现,发现了几个问题:
- element占用空间太大了;
- tab 切换的时候会出现空白的html element残留;
通过unlink tab以及style自定义解决这些问题:
const localgraph = graphLeaf.view.containerEl.getElementsByClassName('view-content')[0];
localgraph.style.width = '80%';
localgraph.style.height = '360px';
localgraph.style.alignSelf = 'center';
localgraph.addClass('embed-local-graph-with-personal-assistant');
// unlink
graphLeaf.tabHeaderStatusLinkEl.click();
const noteHeader = fileLeaf.containerEl.getElementsByClassName('view-header')[0];
noteHeader.parentElement.insertAfter(localgraph, noteHeader);
3. tab切换问题
obsidian tab切换的时候会发现几个问题:
- 重复插入canvas;
- graph view配置menu默认打开;
- graph view没有继承颜色等配置;
await this.app.commands.executeCommandById("personal-assistant:set-local-graph-view-colors");
fileLeaf.view.containerEl.getElementsByClassName('graph-controls-button')[0].click();
this.app.workspace.on('file-open', (file) => {
// console.log(file);
// let fileName = file ? file.basename : "--";
// console.log(fileName);
// if (this.app.workspace.activeLeaf.getDisplayText() !== fileName) {
// console.log("back"); return;
// }
const wins = BrowserWindow.getAllWindows();
const graphWindow2Close = wins.find((win) => {
//return win.getTitle().startsWith("Graph") && win.id === mainWinID;
return !win.isVisible()
},);
if (graphWindow2Close) {
console.log(graphWindow2Close.getTitle());
graphWindow2Close.close();
} else {
fileLeaf.view.containerEl.getElementsByClassName('embed-local-graph-with-personal-assistant')[0]?.remove();
}
},);
4. 功能触发
通过dataview可以借助在任意笔记文件中执行js的能力实现该功能的触发,这样可以保证在打开笔记文件的时候就会触发local graph集成的。
后续
当前这样的实现的思路勉强可以满足需求,但是还有几个缺陷不足:
- dataviewjs 触发执行的时机是lazy的,这会导致打开文件之后插入canvas的动作有滞后;
- 由于electron window接口的问题,会导致editor/preview视图切换的时候出现显示问题;
- 偶尔导致obsidian app闪退;
对于这样的问题看是需要通过插件来实现,后续我打算把这个功能集成personal assistant中做到自动化开关和配置。
附录
// 注意在 obsidian 中通过 `dataviewjs` 来触发,也可以配置到模版中
const { BrowserWindow } = require("@electron/remote");
const fileLeaf = this.app.workspace.activeLeaf;
if (fileLeaf.view.containerEl.getElementsByClassName('embed-local-graph-with-personal-assistant').length > 0) {
console.log("already embedded");
return;
}
const mainWin = BrowserWindow.getAllWindows();
if (mainWin.length !== 1) {
// new file tab to embed local graph
for (let i = 0; i < mainWin.length; i++) {
if (mainWin[i].getTitle().startsWith("Graph of")) {
new Notice("closing embedded local graph windown");
mainWin[i].close();
}
}
return;
}
const mainWinID = mainWin[0].id;
await this.app.commands.executeCommandById("graph:open-local");
//await this.app.commands.executeCommandById("personal-assistant:local-graph");
await new Promise((resolve) => setTimeout(resolve, 100));
const graphLeaf = this.app.workspace.getLeavesOfType('localgraph')[0];
this.app.workspace.moveLeafToPopout(graphLeaf);
const winsFocus = BrowserWindow.getFocusedWindow();
winsFocus.hide();
// unlink
graphLeaf.tabHeaderStatusLinkEl.click();
const localgraph = graphLeaf.view.containerEl.getElementsByClassName('view-content')[0];
localgraph.style.width = '80%';
localgraph.style.height = '360px';
localgraph.style.alignSelf = 'center';
localgraph.addClass('embed-local-graph-with-personal-assistant');
const noteHeader = fileLeaf.containerEl.getElementsByClassName('view-header')[0];
noteHeader.parentElement.insertAfter(localgraph, noteHeader);
await this.app.commands.executeCommandById("personal-assistant:set-local-graph-view-colors");
fileLeaf.view.containerEl.getElementsByClassName('graph-controls-button')[0].click();
this.app.workspace.on('file-open', (file) => {
// console.log(file);
// let fileName = file ? file.basename : "--";
// console.log(fileName);
// if (this.app.workspace.activeLeaf.getDisplayText() !== fileName) {
// console.log("back"); return;
// }
const wins = BrowserWindow.getAllWindows();
const graphWindow2Close = wins.find((win) => {
//return win.getTitle().startsWith("Graph") && win.id === mainWinID;
return !win.isVisible()
},);
if (graphWindow2Close) {
console.log(graphWindow2Close.getTitle());
graphWindow2Close.close();
} else {
fileLeaf.view.containerEl.getElementsByClassName('embed-local-graph-with-personal-assistant')[0]?.remove();
}
},);