=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}var l=f[n],h=r/2+l,d=n+1,p=e-1;for(;d>>1;f[g]c-o){var _=r?(i*v+a*y)/r:a;t(n,d,y,i,o,_,c),t(d,e,v,_,o,a,c)}else{var b=r?(o*v+c*y)/r:c;t(n,d,y,i,o,a,b),t(d,e,v,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Ap,t.treemapResquarify=Lp,t.treemapSlice=Ip,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Ip:Ap)(t,n,e,r,i)},t.treemapSquarify=Yp,t.tsv=Mc,t.tsvFormat=lc,t.tsvFormatBody=hc,t.tsvFormatRow=pc,t.tsvFormatRows=dc,t.tsvFormatValue=gc,t.tsvParse=fc,t.tsvParseRows=sc,t.union=function(...t){const n=new InternSet;for(const e of t)for(const t of e)n.add(t);return n},t.unixDay=_y,t.unixDays=by,t.utcDay=yy,t.utcDays=vy,t.utcFriday=By,t.utcFridays=Vy,t.utcHour=hy,t.utcHours=dy,t.utcMillisecond=Wg,t.utcMilliseconds=Zg,t.utcMinute=cy,t.utcMinutes=fy,t.utcMonday=qy,t.utcMondays=jy,t.utcMonth=Qy,t.utcMonths=Jy,t.utcSaturday=Yy,t.utcSaturdays=Wy,t.utcSecond=iy,t.utcSeconds=oy,t.utcSunday=Fy,t.utcSundays=Ly,t.utcThursday=Oy,t.utcThursdays=Gy,t.utcTickInterval=av,t.utcTicks=ov,t.utcTuesday=Uy,t.utcTuesdays=Hy,t.utcWednesday=Iy,t.utcWednesdays=Xy,t.utcWeek=Fy,t.utcWeeks=Ly,t.utcYear=ev,t.utcYears=rv,t.variance=x,t.version="7.9.0",t.window=pn,t.xml=Sc,t.zip=function(){return gt(arguments)},t.zoom=function(){var t,n,e,r=Ew,i=Nw,o=zw,a=Cw,u=Pw,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=ri,h=$t("start","zoom","end"),d=500,p=150,g=0,y=10;function v(t){t.property("__zoom",kw).on("wheel.zoom",T,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",S).filter(u).on("touchstart.zoom",E).on("touchmove.zoom",N).on("touchend.zoom touchcancel.zoom",k).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new ww(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new ww(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),u=i.apply(t,o),c=null==e?m(u):"function"==typeof e?e.apply(t,o):e,f=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),s=t.__zoom,h="function"==typeof n?n.apply(t,o):n,d=l(s.invert(c).concat(f/s.k),h.invert(c).concat(f/h.k));return function(t){if(1===t)t=h;else{var n=d(t),e=f/n[2];t=new ww(e,c[0]-n[0]*e,c[1]-n[1]*e)}a.zoom(null,t)}}))}function w(t,n,e){return!e&&t.__zooming||new M(t,n)}function M(t,n){this.that=t,this.args=n,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,n),this.taps=0}function T(t,...n){if(r.apply(this,arguments)){var e=w(this,n).event(t),i=this.__zoom,u=Math.max(c[0],Math.min(c[1],i.k*Math.pow(2,a.apply(this,arguments)))),s=ne(t);if(e.wheel)e.mouse[0][0]===s[0]&&e.mouse[0][1]===s[1]||(e.mouse[1]=i.invert(e.mouse[0]=s)),clearTimeout(e.wheel);else{if(i.k===u)return;e.mouse=[s,i.invert(s)],Gi(this),e.start()}Sw(t),e.wheel=setTimeout((function(){e.wheel=null,e.end()}),p),e.zoom("mouse",o(b(_(i,u),e.mouse[0],e.mouse[1]),e.extent,f))}}function A(t,...n){if(!e&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,n,!0).event(t),u=Zn(t.view).on("mousemove.zoom",(function(t){if(Sw(t),!a.moved){var n=t.clientX-s,e=t.clientY-l;a.moved=n*n+e*e>g}a.event(t).zoom("mouse",o(b(a.that.__zoom,a.mouse[0]=ne(t,i),a.mouse[1]),a.extent,f))}),!0).on("mouseup.zoom",(function(t){u.on("mousemove.zoom mouseup.zoom",null),ue(t.view,a.moved),Sw(t),a.event(t).end()}),!0),c=ne(t,i),s=t.clientX,l=t.clientY;ae(t.view),Aw(t),a.mouse=[c,this.__zoom.invert(c)],Gi(this),a.start()}}function S(t,...n){if(r.apply(this,arguments)){var e=this.__zoom,a=ne(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(a),c=e.k*(t.shiftKey?.5:2),l=o(b(_(e,c),a,u),i.apply(this,n),f);Sw(t),s>0?Zn(this).transition().duration(s).call(x,l,a,t):Zn(this).call(v.transform,l,a,t)}}function E(e,...i){if(r.apply(this,arguments)){var o,a,u,c,f=e.touches,s=f.length,l=w(this,i,e.changedTouches.length===s).event(e);for(Aw(e),a=0;a
+
+
+
+ Force-Directed Tree
+
+
+
+
+
+
+
diff --git a/addon/chrome/content/styles/relation.css b/addon/chrome/content/styles/relation.css
new file mode 100644
index 0000000..ac41ee9
--- /dev/null
+++ b/addon/chrome/content/styles/relation.css
@@ -0,0 +1,5 @@
+#bn-relation-graph {
+ width: 100%;
+ height: 250px;
+ border-radius: 8px;
+}
diff --git a/addon/locale/en-US/noteRelation.ftl b/addon/locale/en-US/noteRelation.ftl
new file mode 100644
index 0000000..8efeb8f
--- /dev/null
+++ b/addon/locale/en-US/noteRelation.ftl
@@ -0,0 +1,6 @@
+note-relation-header =
+ .label = Relation Graph
+note-relation-sidenav =
+ .tooltiptext = Relation Graph
+note-relation-refresh =
+ .tooltiptext = Refresh
diff --git a/addon/locale/it-IT/noteRelation.ftl b/addon/locale/it-IT/noteRelation.ftl
new file mode 100644
index 0000000..8efeb8f
--- /dev/null
+++ b/addon/locale/it-IT/noteRelation.ftl
@@ -0,0 +1,6 @@
+note-relation-header =
+ .label = Relation Graph
+note-relation-sidenav =
+ .tooltiptext = Relation Graph
+note-relation-refresh =
+ .tooltiptext = Refresh
diff --git a/addon/locale/ru-RU/noteRelation.ftl b/addon/locale/ru-RU/noteRelation.ftl
new file mode 100644
index 0000000..8efeb8f
--- /dev/null
+++ b/addon/locale/ru-RU/noteRelation.ftl
@@ -0,0 +1,6 @@
+note-relation-header =
+ .label = Relation Graph
+note-relation-sidenav =
+ .tooltiptext = Relation Graph
+note-relation-refresh =
+ .tooltiptext = Refresh
diff --git a/addon/locale/tr-TR/noteRelation.ftl b/addon/locale/tr-TR/noteRelation.ftl
new file mode 100644
index 0000000..8efeb8f
--- /dev/null
+++ b/addon/locale/tr-TR/noteRelation.ftl
@@ -0,0 +1,6 @@
+note-relation-header =
+ .label = Relation Graph
+note-relation-sidenav =
+ .tooltiptext = Relation Graph
+note-relation-refresh =
+ .tooltiptext = Refresh
diff --git a/addon/locale/zh-CN/noteRelation.ftl b/addon/locale/zh-CN/noteRelation.ftl
new file mode 100644
index 0000000..cd201ad
--- /dev/null
+++ b/addon/locale/zh-CN/noteRelation.ftl
@@ -0,0 +1,6 @@
+note-relation-header =
+ .label = 关系图
+note-relation-sidenav =
+ .tooltiptext = 关系图
+note-relation-refresh =
+ .tooltiptext = 刷新
diff --git a/scripts/build-extras.mjs b/scripts/build-extras.mjs
index d00cda6..39f4f40 100644
--- a/scripts/build-extras.mjs
+++ b/scripts/build-extras.mjs
@@ -5,9 +5,9 @@ const buildDir = "build";
export async function main() {
await build({
- entryPoints: ["./src/extras/*.ts"],
+ entryPoints: ["./src/extras/*.*"],
outdir: path.join(buildDir, "addon/chrome/content/scripts"),
bundle: true,
- target: ["firefox102"],
+ target: ["firefox115"],
}).catch(() => exit(1));
}
diff --git a/scripts/build.mjs b/scripts/build.mjs
index 495ef39..f485705 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -194,7 +194,7 @@ export const esbuildOptions = {
__env__: `"${env.NODE_ENV}"`,
},
bundle: true,
- target: "firefox102",
+ target: "firefox115",
outfile: path.join(
buildDir,
`addon/chrome/content/scripts/${config.addonRef}.js`,
diff --git a/src/elements/detailsPane.ts b/src/elements/detailsPane.ts
index b1090ee..6104a7f 100644
--- a/src/elements/detailsPane.ts
+++ b/src/elements/detailsPane.ts
@@ -23,6 +23,7 @@ export class NoteDetails extends ItemDetails {
init() {
MozXULElement.insertFTLIfNeeded(`${config.addonRef}-notePreview.ftl`);
+ MozXULElement.insertFTLIfNeeded(`${config.addonRef}-noteRelation.ftl`);
super.init();
}
diff --git a/src/hooks.ts b/src/hooks.ts
index 97015c0..6434917 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -37,6 +37,7 @@ import { initSyncList } from "./modules/sync/api";
import { patchViewItems } from "./modules/viewItems";
import { onUpdateRelated } from "./modules/relatedNotes";
import { getFocusedWindow } from "./utils/window";
+import { registerNoteRelation } from "./modules/workspace/relation";
async function onStartup() {
await Promise.all([
@@ -58,6 +59,8 @@ async function onStartup() {
registerReaderAnnotationButton();
+ registerNoteRelation();
+
initSyncList();
setSyncing();
diff --git a/src/modules/workspace/relation.ts b/src/modules/workspace/relation.ts
new file mode 100644
index 0000000..4020764
--- /dev/null
+++ b/src/modules/workspace/relation.ts
@@ -0,0 +1,139 @@
+import { config } from "../../../package.json";
+import { slice } from "../../utils/str";
+import { waitUtilAsync } from "../../utils/wait";
+
+export function registerNoteRelation() {
+ const key = Zotero.ItemPaneManager.registerSection({
+ paneID: `bn-note-relation`,
+ pluginID: config.addonID,
+ header: {
+ icon: `chrome://${config.addonRef}/content/icons/relation-16.svg`,
+ l10nID: `${config.addonRef}-note-relation-header`,
+ },
+ sidenav: {
+ icon: `chrome://${config.addonRef}/content/icons/relation-20.svg`,
+ l10nID: `${config.addonRef}-note-relation-sidenav`,
+ },
+ bodyXHTML: `
+
+
+
+`,
+ sectionButtons: [
+ {
+ type: "refreshGraph",
+ icon: "chrome://zotero/skin/16/universal/sync.svg",
+ l10nID: `${config.addonRef}-note-relation-refresh`,
+ onClick: ({ body, item }) => {
+ refresh(body, item);
+ },
+ },
+ ],
+ onInit({ body }) {
+ body
+ .querySelector("iframe")!
+ .contentWindow?.addEventListener("message", (ev) => {
+ if (ev.data.type === "openNote") {
+ addon.hooks.onOpenNote(
+ ev.data.id,
+ ev.data.isShift ? "window" : "tab",
+ );
+ }
+ });
+ },
+ onItemChange: ({ body, setEnabled }) => {
+ if (body.closest("bn-workspace") as HTMLElement | undefined) {
+ setEnabled(true);
+ return;
+ }
+ setEnabled(false);
+ },
+ onRender: () => {},
+ onAsyncRender: async ({ body, item }) => {
+ await refresh(body, item);
+ },
+ });
+}
+
+async function refresh(body: HTMLElement, item: Zotero.Item) {
+ const data = await getRelationData(item);
+ await waitUtilAsync(
+ () =>
+ body.querySelector("iframe")!.contentDocument?.readyState === "complete",
+ );
+ body.querySelector("iframe")!.contentWindow?.postMessage(
+ {
+ type: "render",
+ graph: data,
+ },
+ "*",
+ );
+}
+
+async function getRelationData(note: Zotero.Item) {
+ if (!note) return;
+ const currentContent = note.getNote();
+ const currentLink = addon.api.convert.note2link(note);
+ const currentTitle = slice(note.getNoteTitle(), 15);
+ const { detectedIDSet, currentIDSet } =
+ await addon.api.related.getRelatedNoteIds(note.id);
+ if (!areSetsEqual(detectedIDSet, currentIDSet)) {
+ await addon.api.related.updateRelatedNotes(note.id);
+ }
+ const items = Zotero.Items.get(Array.from(detectedIDSet));
+
+ const nodes = [];
+ const links = [];
+ for (const item of items) {
+ const compareContent = item.getNote();
+ const compareLink = addon.api.convert.note2link(item);
+ const compareTitle = slice(item.getNoteTitle(), 15);
+
+ if (currentLink && compareContent.includes(currentLink)) {
+ links.push({
+ source: item.id,
+ target: note.id,
+ value: 1,
+ });
+ }
+ if (compareLink && currentContent.includes(compareLink)) {
+ links.push({
+ source: note.id,
+ target: item.id,
+ value: 1,
+ });
+ }
+
+ nodes.push({
+ id: item.id,
+ title: compareTitle,
+ group: 2,
+ });
+ }
+
+ nodes.push({
+ id: note.id,
+ title: currentTitle,
+ group: 1,
+ });
+
+ return { nodes, links };
+}
+
+function areSetsEqual(set1: Set, set2: Set): boolean {
+ if (set1.size !== set2.size) {
+ return false;
+ }
+ for (const item of set1) {
+ if (!set2.has(item)) {
+ return false;
+ }
+ }
+ return true;
+}