working on setting

This commit is contained in:
Hananoshika Yomaru 2023-10-24 11:53:34 -07:00
parent 690e026ed8
commit 861ceeb5d4
22 changed files with 469 additions and 189 deletions

View file

@ -11,38 +11,38 @@ if you want to view the source, please visit the github repository of this plugi
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
});
if (prod) {
await context.rebuild();
process.exit(0);
await context.rebuild();
process.exit(0);
} else {
await context.watch();
await context.watch();
}

View file

@ -1,4 +1,8 @@
import { BasicSearchEngine } from "@/BasicSearchEngine";
import { DvSearchEngine } from "@/DvSearchEngine";
import { IFileManager, IPassiveSearchEngine, IActiveSearchEngine } from "@/Interfaces";
import { PassiveSearchEngine } from "@/PassiveSearchEngine";
import { SearchEngineType } from "@/SettingsSchemas";
import Graph3dPlugin from "@/main";
/**
@ -8,9 +12,21 @@ export class MyFileManager implements IFileManager {
private plugin: Graph3dPlugin;
public searchEngine: IActiveSearchEngine | IPassiveSearchEngine;
constructor(plugin: Graph3dPlugin, searchEngine: IActiveSearchEngine | IPassiveSearchEngine) {
constructor(plugin: Graph3dPlugin) {
this.plugin = plugin;
this.searchEngine = searchEngine;
this.setSearchEngine();
}
/**
* this will set the search engine base on the setting
*/
setSearchEngine() {
const searchEngine = this.plugin.settingManager.getSettings().pluginSetting.searchEngine;
if (searchEngine === SearchEngineType.default)
this.searchEngine = new BasicSearchEngine(this.plugin);
else if (searchEngine === SearchEngineType.dataview)
this.searchEngine = new DvSearchEngine(this.plugin);
else this.searchEngine = new PassiveSearchEngine(this.plugin);
}
getFiles() {
@ -26,8 +42,7 @@ export class MyFileManager implements IFileManager {
// check whether it is active search engine
if (this.searchEngine instanceof IActiveSearchEngine) {
return this.searchEngine.searchFiles(this.searchEngine.parseQueryToConfig(query));
} else {
return this.searchEngine.getFiles();
}
throw new Error("passive search engine cannot search files");
}
}

View file

@ -1,4 +1,4 @@
import { TAbstractFile } from "obsidian";
import { SearchView, TAbstractFile } from "obsidian";
/**
* the config object of the search engine
@ -53,15 +53,11 @@ export abstract class IActiveSearchEngine extends ISearchEngine {
}
export abstract class IPassiveSearchEngine extends ISearchEngine {
/**
* when the result is change, this callback will be called.
* This mutation should only be fired when the result is stable
*/
abstract mutationCallback: (files: TAbstractFile[]) => void;
/**
* an active way to call it
*/
abstract getFiles(): TAbstractFile[];
abstract addMutationObserver(
searchResultContainerEl: HTMLDivElement,
view: SearchView,
mutationCallback: (files: TAbstractFile[]) => void
): void;
}
export interface IFileManager {

View file

@ -1,20 +1,54 @@
import { IPassiveSearchEngine } from "@/Interfaces";
import { TAbstractFile } from "obsidian";
import Graph3dPlugin from "@/main";
import { waitForStable } from "@/util/waitFor";
import { SearchView, TAbstractFile, TFile } from "obsidian";
export type SearchResultFile = ReturnType<typeof getFilesFromSearchResult>[0];
/**
* given the result from `getResultFromSearchView`, return the files
*/
const getFilesFromSearchResult = (rawSearchResult: unknown) => {
// @ts-ignore
return Array.from(rawSearchResult.keys()) as TFile[];
};
const getResultFromSearchView = async (searchView: SearchView) => {
await waitForStable(() => {
return searchView.dom.resultDomLookup.size;
}, {});
return searchView.dom.resultDomLookup;
};
/**
* this is the built in search engine that uses the obsidian search engine
*/
export class PassiveSearchEngine implements IPassiveSearchEngine {
useBuiltInSearchInput = true;
mutationCallback: (files: TAbstractFile[]) => void;
plugin: Graph3dPlugin;
constructor(mutationCallback: (files: TAbstractFile[]) => void) {
this.mutationCallback = mutationCallback;
// init the listeners for the changes
constructor(plugin: Graph3dPlugin) {
this.plugin = plugin;
}
getFiles(): TAbstractFile[] {
throw new Error("Method not implemented.");
/**
* given a search result container element, add a mutation observer to it
*/
addMutationObserver(
searchResultContainerEl: HTMLDivElement,
view: SearchView,
mutationCallback: (files: TAbstractFile[]) => void
) {
const observer = new MutationObserver(async (mutations) => {
// if the search result is loading or the cache is not ready, then we know that the search must not be ready yet
if (searchResultContainerEl.classList.contains("is-loading") || !this.plugin.cacheIsReady)
return;
const files = getFilesFromSearchResult(await getResultFromSearchView(view));
mutationCallback(files);
});
observer.observe(searchResultContainerEl, {
childList: true,
subtree: true,
});
}
}

View file

@ -59,6 +59,9 @@ export class MySettingManager implements ISettingManager<Setting> {
*/
private isLoaded = false;
/**
* @remarks don't forget to call `loadSettings` after creating this class
*/
constructor(plugin: Graph3dPlugin) {
this.plugin = plugin;
}

View file

@ -14,7 +14,7 @@ export enum GraphType {
export enum SearchEngineType {
dataview = "dataview",
default = "default",
builtIn = "built-in",
builtIn = "builtIn",
}
export enum DagOrientation {

View file

@ -1,6 +1,6 @@
import { Link } from "@/graph/Link";
import { Node } from "@/graph/Node";
import { App } from "obsidian";
import { App, TAbstractFile } from "obsidian";
export class Graph {
public readonly nodes: Node[];
@ -77,43 +77,62 @@ export class Graph {
}
/**
* This method retrieves the local graph of a node, which includes all the other nodes and links
* that are connected to the given node recursively based on the specified depth and link type.
*
* @param nodeId
* @returns the local graph of a node
*/
public getLocalGraph(nodeId: string): Graph {
const node = this.getNodeById(nodeId);
if (node) {
const nodes = [node, ...node.neighbors];
const links: Link[] = [];
const nodeIndex = new Map<string, number>();
public getLocalGraph(
param: (
| {
id: string;
}
| {
path: string;
}
) & {
depth: number;
linkType: "both" | "inlinks" | "outlinks";
}
): Graph {
const node = "id" in param ? this.getNodeById(param.id) : this.getNodeByPath(param.path);
nodes.forEach((node, index) => {
nodeIndex.set(node.id, index);
});
const nodes: Node[] = [];
const links: Link[] = [];
const nodeIndex = new Map<string, number>();
nodes.forEach((node, index) => {
const filteredLinks = node.links
.filter((link) => nodeIndex.has(link.target.id) && nodeIndex.has(link.source.id))
.map((link) => {
if (
!links.includes(link) &&
nodeIndex.has(link.target.id) &&
nodeIndex.has(link.source.id)
)
links.push(link);
return link;
});
node.links.splice(0, node.links.length, ...filteredLinks);
});
const linkIndex = Link.createLinkIndex(links);
return new Graph(nodes, links, nodeIndex, linkIndex);
} else {
if (!node) {
return new Graph([], [], new Map(), new Map());
}
const traverseNeighbors = (currentNode: Node, currentDepth: number) => {
if (currentDepth > param.depth || nodeIndex.has(currentNode.id)) return; // Depth limit reached or node already traversed
nodes.push(currentNode);
nodeIndex.set(currentNode.id, nodes.length - 1);
currentNode.links.forEach((link) => {
if (
param.linkType === "both" ||
(param.linkType === "inlinks" && link.target.id === currentNode.id) ||
(param.linkType === "outlinks" && link.source.id === currentNode.id)
) {
if (!links.includes(link)) {
links.push(link);
traverseNeighbors(
link.target.id === currentNode.id ? link.source : link.target,
currentDepth + 1
);
}
}
});
};
traverseNeighbors(node, 0);
const linkIndex = Link.createLinkIndex(links);
return new Graph(nodes, links, nodeIndex, linkIndex);
}
// Clones the graph
@ -126,6 +145,10 @@ export class Graph {
);
};
public static createEmpty = (): Graph => {
return new Graph([], [], new Map(), new Map());
};
// Creates a graph using the Obsidian API
public static createFromApp = (app: App): Graph => {
const [nodes, nodeIndex] = Node.createFromFiles(app.vault.getFiles()),
@ -133,6 +156,16 @@ export class Graph {
return new Graph(nodes, links, nodeIndex, linkIndex);
};
public static createFromFiles = (files: TAbstractFile[], app: App): Graph => {
const [nodes, nodeIndex] = Node.createFromFiles(files),
[links, linkIndex] = Link.createFromCache(app.metadataCache.resolvedLinks, nodes, nodeIndex);
const tempGraph = new Graph(nodes, links, nodeIndex, linkIndex);
// const tempGraph = Graph.createFromApp(app);
// since the nodes are from the files, they are already correct.
// we just need to update the links, we can pass into Boolean as the predicate
return this.createFromMask(Boolean, tempGraph);
};
// updates this graph with new data from the Obsidian API
public update = (app: App) => {
const newGraph = Graph.createFromApp(app);
@ -153,12 +186,13 @@ export class Graph {
/**
* filter the nodes of the graph, the links will be filtered automatically.
* @param predicate this
* @param predicate what nodes to keep
* @param graph the graph to filter
* @returns a new graph
*/
public filter = (predicate: (node: Node) => boolean) => {
const filteredNodes = this.nodes.filter(predicate);
const filteredLinks = this.links.filter((link) => {
public static createFromMask = (predicate: (node: Node) => boolean, graph: Graph) => {
const filteredNodes = graph.nodes.filter(predicate);
const filteredLinks = graph.links.filter((link) => {
// the source and target nodes of a link must be in the filtered nodes
return (
filteredNodes.some((node) => link.source.id === node.id) &&
@ -175,4 +209,44 @@ export class Graph {
return new Graph(filteredNodes, filteredLinks, nodeIndex, linkIndex);
};
public static compare = (graph1: Graph, graph2: Graph): boolean => {
if (graph1.nodes.length !== graph2.nodes.length) {
return false;
}
if (graph1.links.length !== graph2.links.length) {
return false;
}
const graph2NodeIds = new Set(graph2.nodes.map((node) => node.id));
// Check if all nodes in graph1 exist in graph2
for (const node1 of graph1.nodes) {
if (!graph2NodeIds.has(node1.id)) {
return false;
}
}
function getLinkId(link: Link): string {
return `${link.source.path}-${link.target.path}`;
}
const graph2LinkIds = new Set(graph2.links.map(getLinkId));
// Check if all links in graph1 exist in graph2
for (const link1 of graph1.links) {
if (!graph2LinkIds.has(getLinkId(link1))) {
return false;
}
}
return true;
};
/**
* get the files from the graph
*/
public static getFiles(app: App, graph: Graph): TAbstractFile[] {
return graph.nodes.map((node) => app.vault.getAbstractFileByPath(node.path)).filter(Boolean);
}
}

View file

@ -72,4 +72,8 @@ export class Link {
return [links, Link.createLinkIndex(links)];
}
public static compare = (a: Link, b: Link) => {
return a.source.path === b.source.path && a.target.path === b.target.path;
};
}

View file

@ -1,5 +1,5 @@
import { Link } from "@/graph/Link";
import { TFile } from "obsidian";
import { TAbstractFile } from "obsidian";
export class Node {
public readonly id: string;
@ -20,7 +20,7 @@ export class Node {
}
// Creates an array of nodes from an array of files (from the Obsidian API)
static createFromFiles(files: TFile[]): [Node[], Map<string, number>] {
static createFromFiles(files: TAbstractFile[]): [Node[], Map<string, number>] {
const nodeMap = new Map<string, number>();
return [
files
@ -64,4 +64,8 @@ export class Node {
if (node instanceof Node) return this.neighbors.includes(node);
else return this.neighbors.some((neighbor) => neighbor.id === node);
}
public static compare = (a: Node, b: Node) => {
return a.path === b.path;
};
}

View file

@ -10,16 +10,14 @@ import { eventBus } from "@/util/EventBus";
import { SettingTab } from "@/settings/SettingTab";
import { config } from "@/config";
import { MyFileManager } from "@/FileManager";
import { BasicSearchEngine } from "@/BasicSearchEngine";
import { DvSearchEngine } from "@/DvSearchEngine";
import { MySettingManager } from "@/SettingManager";
import { GraphType, SearchEngineType } from "@/SettingsSchemas";
import { GraphType } from "@/SettingsSchemas";
import { SearchManager } from "@/SearchManager";
import { NewGraph3dView } from "@/views/graph/NewGraph3dView";
export default class Graph3dPlugin extends Plugin {
_resolvedCache: ResolvedLinkCache;
private cacheIsReady: State<boolean> = new State(
public readonly cacheIsReady: State<boolean> = new State(
this.app.metadataCache.resolvedLinks !== undefined
);
/**
@ -42,18 +40,13 @@ export default class Graph3dPlugin extends Plugin {
this.settingManager = new MySettingManager(this);
// load the setting using setting manager
const settings = await this.settingManager.loadSettings();
await this.settingManager.loadSettings();
// get the setting from setting manager
// const setting = this.settingManager.getSetting("test");
// initalise the file manager
this.fileManager = new MyFileManager(
this,
settings.pluginSetting.searchEngine === SearchEngineType.dataview
? new DvSearchEngine(this)
: new BasicSearchEngine(this)
);
this.fileManager = new MyFileManager(this);
// init the theme
this.theme = new ObsidianTheme(this.app.workspace.containerEl);
@ -110,12 +103,19 @@ export default class Graph3dPlugin extends Plugin {
);
}
/**
* this will be called the when the cache is ready.
* And this will hit the else clause of the `onGraphCacheChanged` function
*/
private onGraphCacheReady = () => {
console.log("Graph cache is ready");
this.cacheIsReady.value = true;
this.onGraphCacheChanged();
};
/**
* check if the cache is ready and if it is, update the global graph
*/
public onGraphCacheChanged = () => {
// check if the cache actually updated
// Obsidian API sends a lot of (for this plugin) unnecessary stuff
@ -133,6 +133,15 @@ export default class Graph3dPlugin extends Plugin {
" and ",
deepCompare(this._resolvedCache, this.app.metadataCache.resolvedLinks)
);
// update global graph view
this.activeGraphViews.forEach((view) => {
if (view.graphType === GraphType.global) {
view.updateGraphData({
files: Graph.getFiles(this.app, this.globalGraph),
});
}
});
}
};

View file

@ -1,9 +1,6 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import Graph3dPlugin from "@/main";
import { SearchEngineType } from "@/SettingsSchemas";
import { BasicSearchEngine } from "@/BasicSearchEngine";
import { DvSearchEngine } from "@/DvSearchEngine";
import { PassiveSearchEngine } from "@/PassiveSearchEngine";
const DEFAULT_NUMBER = 200;
@ -59,7 +56,9 @@ export class SettingTab extends PluginSettingTab {
.setDesc("Search engine determine how to parse the query string and return results.")
.addDropdown((dropdown) => {
dropdown
.addOptions(SearchEngineType)
.addOptions({
[SearchEngineType.builtIn]: SearchEngineType.builtIn,
})
// you need to add options before set value
.setValue(pluginSetting.searchEngine)
.onChange(async (value: SearchEngineType) => {
@ -70,14 +69,7 @@ export class SettingTab extends PluginSettingTab {
});
// update the plugin file manager
this.plugin.fileManager.searchEngine =
value === SearchEngineType.default
? new BasicSearchEngine(this.plugin)
: value === SearchEngineType.dataview
? new DvSearchEngine(this.plugin)
: new PassiveSearchEngine((files) => {
console.log(files);
});
this.plugin.fileManager.setSearchEngine();
// force all the graph view to reset their settings
this.plugin.activeGraphViews.forEach((view) => view.settingManager.resetSettings());

View file

@ -67,7 +67,6 @@ export function waitForStable<T = unknown>(
clearInterval(intervalId);
resolve(currentValue);
}
console.log(previousValue, currentValue);
previousValue = currentValue;
}, interval);
});

View file

@ -1,8 +1,11 @@
import Graph3dPlugin from "@/main";
import { PassiveSearchEngine } from "@/PassiveSearchEngine";
import { NewGraph3dView } from "@/views/graph/NewGraph3dView";
import { spawnLeafView } from "@/views/leafView";
import { TextComponent } from "obsidian";
import { SearchView, TAbstractFile, TextComponent } from "obsidian";
/**
* depends on the search engine, this might be a normal text input or a built-in search input
*
* @remarks
* the search input doesn't perform the search, it only display the input
*/
@ -21,12 +24,12 @@ export const addSearchInput = async (
* @param files the files that match the query
*/
onChange: (value: string) => void,
plugin: Graph3dPlugin
view: NewGraph3dView
) => {
const searchEl = containerEl.createDiv({
// cls :
});
if (!plugin.fileManager.searchEngine.useBuiltInSearchInput) {
if (!view.plugin.fileManager.searchEngine.useBuiltInSearchInput) {
const text = new TextComponent(searchEl).setValue(value).onChange((value) => {
onChange(value);
});
@ -38,7 +41,7 @@ export const addSearchInput = async (
return;
}
const [searchLeaf] = spawnLeafView(plugin, searchEl);
const [searchLeaf] = spawnLeafView(view.plugin, searchEl);
await searchLeaf.setViewState({
type: "search",
@ -56,7 +59,7 @@ export const addSearchInput = async (
".search-result-container"
) as HTMLDivElement;
const searchResultInfoEl = searchElement.querySelector(".search-results-info") as HTMLDivElement;
// const parentEl = searchResultContainerEl.parentElement!;
searchResultContainerEl.style.visibility = "hidden";
searchResultContainerEl.style.height = "0px";
searchResultContainerEl.style.position = "absolute";
@ -77,10 +80,9 @@ export const addSearchInput = async (
settingIconEl?.remove();
matchCaseIconEl?.remove();
inputEl.value = value;
inputEl.onkeydown = async (e) => {
// const query = inputEl.value;
// const files = plugin.fileManager.searchFiles(query);
onChange(inputEl.value);
inputEl.oninput = async (e: Event<HTMLInputElement>) => {
// @ts-ignore
onChange(e.currentTarget?.value);
};
clearButtonEl.onclick = () => {
@ -89,5 +91,22 @@ export const addSearchInput = async (
onChange("");
};
return [searchRowEl] as const;
// this make search that the search result container el is alaways visible
view.containerEl.appendChild(searchResultContainerEl);
// if it is a passive engine, we need to enable mutation observer
const addMutationObserver = (callback: (files: TAbstractFile[]) => void) => {
if (view.plugin.fileManager.searchEngine instanceof PassiveSearchEngine)
view.plugin.fileManager.searchEngine.addMutationObserver(
searchResultContainerEl,
searchLeaf.view as SearchView,
callback
);
else {
throw new Error("cannot add mutation observer to active search engine");
}
};
return { searchRowEl, addMutationObserver };
};

View file

@ -1,5 +1,5 @@
import * as THREE from "three";
import { origin } from "./NewForceGraph";
import { origin } from "@/views/graph/NewForceGraph";
export class CenterCoordinates {
public readonly arrowsGroup = new THREE.Group();

View file

@ -13,6 +13,13 @@ import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js";
*/
export const origin = new THREE.Vector3(0, 0, 0);
type MyForceGraph3DInstance = Omit<ForceGraph3DInstance, "graphData"> & {
graphData: {
(): Graph; // When no argument is passed, it returns a Graph
(graph: Graph): MyForceGraph3DInstance; // When a Graph is passed, it returns MyForceGraph3DInstance
};
};
/**
* this class control the config and graph of the force graph. The interaction is not control here.
*/
@ -20,15 +27,13 @@ export class NewForceGraph {
private view: NewGraph3dView;
private config: LocalGraphSettings | GlobalGraphSettings;
private graph: Graph;
private instance: ForceGraph3DInstance;
public readonly instance: MyForceGraph3DInstance;
private centerCoordinates: CenterCoordinates;
/**
*
* this will create a new force graph instance and render it to the view
* @param view
* @param graph
* @param config you have to provide the full config here!!
*/
constructor(
@ -38,7 +43,7 @@ export class NewForceGraph {
) {
this.view = view;
this.config = config;
this.graph = graph;
// test;
// get the content element of the item view
const rootHtmlElement = view.contentEl as HTMLDivElement;
@ -62,12 +67,13 @@ export class NewForceGraph {
}),
],
})(rootHtmlElement)
.graphData(graph)
// the options here are auto
.width(rootHtmlElement.innerWidth)
.height(rootHtmlElement.innerHeight)
.d3Force("collide", d3.forceCollide(5))
// transparent
.backgroundColor(hexToRGBA("#000000", 0));
.backgroundColor(hexToRGBA("#000000", 0)) as unknown as MyForceGraph3DInstance;
const scene = this.instance.scene();
// add others things
@ -94,13 +100,16 @@ export class NewForceGraph {
* given a new force Graph, the update the graph and the instance
*/
public updateGraph(graph: Graph) {
this.updateInstance(graph, undefined);
// some optimization here
// if the graph is the same, then we don't need to update the graph
const same = Graph.compare(this.instance.graphData(), graph);
if (!same) this.updateInstance(graph, undefined);
}
/**
* given the changed things, update the instance
*/
public updateInstance = (graph?: Graph, config?: Partial<SavedSetting["setting"]>) => {
private updateInstance = (graph?: Graph, config?: Partial<SavedSetting["setting"]>) => {
if (graph !== undefined) this.instance.graphData(graph);
if (config?.display?.nodeSize !== undefined)
this.instance.nodeRelSize(config.display?.nodeSize);
@ -120,25 +129,12 @@ export class NewForceGraph {
/**
* derive the need to reheat the simulation
*/
const needReheat =
config?.display?.nodeRepulsion !== undefined ||
config?.display?.linkDistance !== undefined ||
graph !== undefined;
// const needReheat =
// config?.display?.nodeRepulsion !== undefined || config?.display?.linkDistance !== undefined;
if (needReheat) {
this.instance.numDimensions(3); // reheat simulation
this.instance.refresh();
}
// if (needReheat) {
// this.instance.numDimensions(3); // reheat simulation
// this.instance.refresh();
// }
};
public getInstance() {
/**
* patch the graph data so that the type is right.
*
* this should be the only way to access graph data from a force graph instance
*/
return this.instance as unknown as Omit<ForceGraph3DInstance, "graphData"> & {
graphData: () => Graph;
};
}
}

View file

@ -2,16 +2,14 @@ import { GraphType } from "@/SettingsSchemas";
import { config } from "@/config";
import { Graph } from "@/graph/Graph";
import Graph3dPlugin from "@/main";
import { createNotice } from "@/util/createNotice";
import { NewForceGraph } from "@/views/graph/NewForceGraph";
import { GraphSettingManager } from "@/views/settings/GraphSettingsManager";
import { getGraphAfterProcessingConfig } from "@/views/settings/categories/getGraphAfterProcessingConfig";
import { App, ItemView, TAbstractFile, WorkspaceLeaf } from "obsidian";
/**
* This is the entry point for the graph view
*/
const getGraph = (app: App): Graph => {
// TODO: implement this
return Graph.createFromApp(app);
const getGraphFromFiles = (app: App, files: TAbstractFile[]): Graph => {
return Graph.createFromFiles(files, app);
};
export class NewGraph3dView extends ItemView {
@ -25,7 +23,7 @@ export class NewGraph3dView extends ItemView {
/**
* if this is the local graph, this will be the current file ;
*/
private currentFile: TAbstractFile;
public currentFile: TAbstractFile | undefined;
public readonly settingManager: GraphSettingManager;
@ -43,13 +41,14 @@ export class NewGraph3dView extends ItemView {
if (file) {
this.currentFile = file;
// recreate the graph view
const graph = getGraph(this.plugin.app);
this.forceGraph = new NewForceGraph(
this,
graph,
this.plugin.settingManager.getNewSetting(graphType)
);
const graph = getGraphAfterProcessingConfig(this.plugin, {
files: [],
graphType: GraphType.local,
setting: this.settingManager.getCurrentSetting().filter,
centerFile: file,
});
this.updateGraphData({ graph });
// we need to append the setting so that setting will be in front of the graph
this.contentEl.appendChild(this.settingManager.containerEl);
@ -62,20 +61,29 @@ export class NewGraph3dView extends ItemView {
}
}
console.warn("temporarily disabled");
// the view is already initialized, so we can create the graph
// set up some UI stuff
this.contentEl.classList.add("graph-3d-view");
this.settingManager = new GraphSettingManager(this);
const graph =
this.graphType === GraphType.local
? getGraphAfterProcessingConfig(this.plugin, {
files: [],
graphType: GraphType.local,
setting: this.settingManager.getCurrentSetting().filter,
centerFile: this.currentFile,
})
: this.plugin.globalGraph;
// you need to set up the graph before setting so that the setting will be in front of the graph
const graph = getGraph(this.plugin.app);
this.forceGraph = new NewForceGraph(
this,
graph,
this.plugin.settingManager.getNewSetting(graphType)
);
// set up some UI stuff
this.contentEl.classList.add("graph-3d-view");
this.settingManager = new GraphSettingManager(this);
// move the setting to the front of the graph
this.contentEl.appendChild(this.settingManager.containerEl);
}
onload(): void {
@ -85,7 +93,7 @@ export class NewGraph3dView extends ItemView {
onunload(): void {
super.onunload();
this.forceGraph.getInstance()._destructor();
this.forceGraph?.instance._destructor();
this.plugin.activeGraphViews = this.plugin.activeGraphViews.filter((view) => view !== this);
}
@ -106,15 +114,60 @@ export class NewGraph3dView extends ItemView {
if (this.forceGraph) this.forceGraph.updateDimensions();
}
/**
* get the current force graph object
*/
public getForceGraph() {
return this.forceGraph;
}
/**
* destroy the old graph, remove the old graph completely from the DOM.
* reassign a new graph base on setting like the constructor,
* then render it.
*/
public refreshGraph() {
// destroy the old graph, remove the old graph completely from the DOM
// reassign a new graph base on setting like the constructor
// then render it
const graph = this.forceGraph.instance.graphData();
throw new Error("not implemented yet");
// get the first child of the content element
const forceGraphEl = this.contentEl.firstChild;
forceGraphEl?.remove();
// destroy the old graph, remove the old graph completely from the DOM
this.forceGraph.instance._destructor();
// reassign a new graph base on setting like the constructor
this.forceGraph = new NewForceGraph(this, graph, this.settingManager.getCurrentSetting());
// move the setting to the front of the graph
this.contentEl.appendChild(this.settingManager.containerEl);
this.onResize();
}
/**
* given some files and config, update the graph data.
*/
public updateGraphData(
param:
| {
files: TAbstractFile[];
}
| {
graph: Graph;
}
) {
const graph = "files" in param ? getGraphFromFiles(this.app, param.files) : param.graph;
const tooLarge =
graph.nodes.length > this.plugin.settingManager.getSettings().pluginSetting.maxNodeNumber;
if (tooLarge) {
createNotice(`Graph is too large to be rendered. Have ${graph.nodes.length} nodes.`);
}
this.forceGraph.updateGraph(tooLarge ? Graph.createEmpty() : graph);
}
/**
* handle setting update
*/
public handleSettingUpdate = () => {};
}

View file

@ -20,6 +20,8 @@ export class GraphSettingManager {
public readonly containerEl: HTMLDivElement;
private settingsButton: ExtraButtonComponent;
private graphControlsEl: HTMLDivElement;
public filterSettingView: ReturnType<typeof FilterSettingsView>;
public displaySettingView: ReturnType<typeof DisplaySettingsView>;
private currentSetting: State<SavedSetting["setting"]>;
@ -59,7 +61,10 @@ export class GraphSettingManager {
this.graphControlsEl,
this.currentSetting.value.filter,
"Filters",
(...args) => FilterSettingsView(...args, this)
(...args) => {
this.filterSettingView = FilterSettingsView(...args, this);
return this.filterSettingView;
}
);
// add the group settings
@ -69,6 +74,7 @@ export class GraphSettingManager {
"Groups",
(...args) => GroupSettingsView(...args, this.graphView)
);
this.appendSettingGroup(
this.graphControlsEl,
this.currentSetting.value,
@ -173,6 +179,8 @@ export class GraphSettingManager {
// pass the updated graph object or any other new config to update the graph
}
this.graphView.handleSettingUpdate();
return this.currentSetting.value;
}

View file

@ -17,11 +17,10 @@ export const AddNodeGroupItem = async (
*/
index: number
) => {
const plugin = view.plugin;
// This group must exist
const groupEl = containerEl.createDiv({ cls: "graph-color-group" });
await addSearchInput(
const searchInput = await addSearchInput(
groupEl,
newGroup.query,
(value) => {
@ -31,9 +30,14 @@ export const AddNodeGroupItem = async (
return setting;
});
},
plugin
view
);
if (searchInput)
searchInput.addMutationObserver((files) => {
console.log("set the graph config", files);
});
addColorPicker(groupEl, newGroup.color, (value) => {
view.settingManager.updateCurrentSettings((setting) => {
// This group must exist

View file

@ -30,11 +30,11 @@ export const addNodeGroupButton = (
// add a group to group settings
view.settingManager.updateCurrentSettings((setting) => {
setting.groups.push(newGroup);
// add a group to UI as well, add it in the containerEl before the button container el
return setting;
});
// add a group to UI as well, add it in the containerEl before the button container el
const index = groupSettings.length - 1;
// we need to get the latest current setting so that index will be correct
const index = view.settingManager.getCurrentSetting().groups.length - 1;
AddNodeGroupItem(newGroup, containerEl, view, index);
containerEl.append(buttonContainerEl);
});

View file

@ -4,6 +4,7 @@ import { addSearchInput } from "@/views/atomics/addSearchInput";
import { NewGraph3dView } from "@/views/graph/NewGraph3dView";
import { BaseFilterSettings, LocalFilterSetting, LocalGraphSettings } from "@/SettingManager";
import { GraphType } from "@/SettingsSchemas";
import { getGraphAfterProcessingConfig } from "@/views/settings/categories/getGraphAfterProcessingConfig";
export const FilterSettingsView = async (
filterSettings: BaseFilterSettings | LocalFilterSetting,
@ -11,8 +12,7 @@ export const FilterSettingsView = async (
settingManager: NewGraph3dView["settingManager"]
) => {
const graphView = settingManager.getGraphView();
const plugin = graphView.plugin;
await addSearchInput(
const searchInput = await addSearchInput(
containerEl,
filterSettings.searchQuery,
(value) => {
@ -22,9 +22,23 @@ export const FilterSettingsView = async (
return setting;
});
},
plugin
graphView
);
// if this is a built-in search input, then we need to add a mutation observer
if (searchInput)
searchInput.addMutationObserver((files) => {
// the files is empty, by default, we will show all files
graphView.updateGraphData({
graph: getGraphAfterProcessingConfig(graphView.plugin, {
files,
graphType: graphView.graphType,
setting: settingManager.getCurrentSetting().filter,
centerFile: graphView.currentFile,
}),
});
});
// add show attachments setting
new Setting(containerEl).setName("Show Attachments").addToggle((toggle) => {
toggle.setValue(filterSettings.showAttachments || false).onChange(async (value) => {
@ -82,5 +96,9 @@ export const FilterSettingsView = async (
settingManager.displaySettingView.showDagOrientationSetting();
});
});
return {
searchInput,
};
}
};

View file

@ -0,0 +1,53 @@
import { TAbstractFile } from "obsidian";
import { BaseFilterSettings, LocalFilterSetting } from "@/SettingManager";
import { GraphType } from "@/SettingsSchemas";
import { Graph } from "@/graph/Graph";
import Graph3dPlugin from "@/main";
/**
* the files from the search result or the files from the search engine cannot be used directly
*/
export const getGraphAfterProcessingConfig = (
plugin: Graph3dPlugin,
{
files,
graphType,
setting,
centerFile,
}: {
files: TAbstractFile[];
graphType: GraphType;
setting: BaseFilterSettings | LocalFilterSetting;
centerFile?: TAbstractFile;
}
) => {
if (graphType === GraphType.local && "depth" in setting) {
// then it is a local graph
return Graph.createFromMask(
(node) => {
// if node is an orphan and show orphan is false, then we will not show it
if (node.links.length === 0 && !setting.showOrphans) return false;
// if node is not a markdown and show attachment is false, then we will not show it
if (!node.path.endsWith(".md") && !setting.showAttachments) return false;
// if the node is not in the files, then we will not show it
return files.length === 0 ? true : files.some((file) => file.path === node.path);
},
// active file must exist in local graph
plugin.globalGraph.getLocalGraph({
path: centerFile?.path ?? "",
depth: setting.depth,
linkType: setting.linkType,
})
);
} else {
// if it is global graph
return Graph.createFromMask((node) => {
// if node is an orphan and show orphan is false, then we will not show it
if (node.links.length === 0 && !setting.showOrphans) return false;
// if node is not a markdown and show attachment is false, then we will not show it
if (!node.path.endsWith(".md") && !setting.showAttachments) return false;
// if the node is not in the files, then we will not show it
return files.length === 0 ? true : files.some((file) => file.path === node.path);
}, plugin.globalGraph);
}
};

View file

@ -19,8 +19,7 @@ export const getMySwitcher = (view: NewGraph3dView) => {
const suggestions = await super.getSuggestions(query);
const allFilePaths = view
.getForceGraph()
.getInstance()
.graphData()
.instance.graphData()
.nodes.map((n) => n.path);
return suggestions.filter(Boolean).filter((s) => {
// only show files in this view