mirror of
https://github.com/calfmoon/node-factor.git
synced 2026-07-22 12:20:32 +00:00
Compare commits
39 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5d1538c9 | ||
|
|
b3dfbb089b | ||
|
|
99cb882ff3 | ||
|
|
aa5f5cb5e5 | ||
|
|
dbdf7d2a90 | ||
|
|
8ba7f85b55 | ||
|
|
ca82866158 | ||
|
|
a36f1c9ed0 | ||
|
|
cd5f4c97c9 | ||
|
|
f23e337178 | ||
|
|
9d2b9769c4 | ||
|
|
d02a58be62 | ||
|
|
43ea5c276a | ||
|
|
3c56ee6e74 | ||
|
|
e0279c3845 | ||
|
|
d212d1f57f | ||
|
|
fd824ea406 | ||
|
|
a213c8b3c5 | ||
|
|
aa0987082e | ||
|
|
b43c8dcafc | ||
|
|
2810b9379c | ||
|
|
41636dd29c | ||
|
|
783607292d | ||
|
|
777de23d9a | ||
|
|
a9b433ba2a | ||
|
|
b319d722fd | ||
|
|
7d19749492 | ||
|
|
bc0f7fdcc0 | ||
|
|
cdbc614b2f | ||
|
|
6db8409e6e | ||
|
|
d3663e9de8 | ||
|
|
70b51e9a3d | ||
|
|
90f45a0df2 | ||
|
|
bd933fbd68 | ||
|
|
c5739de84c | ||
|
|
b23deebdf5 | ||
|
|
713d54aeda | ||
|
|
7886168d88 | ||
|
|
11c2d86cf8 |
22 changed files with 4116 additions and 1006 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
node_modules/
|
|
||||||
|
|
||||||
main.js
|
|
||||||
23
.eslintrc
23
.eslintrc
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"root": true,
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"env": { "node": true },
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"no-prototype-builtins": "off",
|
|
||||||
"@typescript-eslint/no-empty-function": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
.github/workflows/lint.yml
vendored
Normal file
28
.github/workflows/lint.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
name: Node.js build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20.x, 22.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: "npm"
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build --if-present
|
||||||
|
- run: npm run lint
|
||||||
|
|
||||||
35
.github/workflows/release.yaml
vendored
Normal file
35
.github/workflows/release.yaml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
name: Release Obsidian plugin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18.x"
|
||||||
|
|
||||||
|
- name: Build plugin
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
tag="${GITHUB_REF#refs/tags/}"
|
||||||
|
|
||||||
|
gh release create "$tag" \
|
||||||
|
--title="$tag" \
|
||||||
|
--draft \
|
||||||
|
main.js manifest.json
|
||||||
22
LICENSE
22
LICENSE
|
|
@ -1,5 +1,21 @@
|
||||||
Copyright (C) 2020-2025 by Dynalist Inc.
|
MIT License
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
Copyright (c) 2021 CalfMoon
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
|
||||||
23
README.md
23
README.md
|
|
@ -1,8 +1,15 @@
|
||||||
# Obsidian Node Factor
|
# Node Factor
|
||||||
This plugin helps you customize the factors that effect node size in obsidian graph view.
|
This plugin helps you customize the factors that effect node size in obsidian graph view.
|
||||||
The factors include, letter count, forward & backward weight, and forward tree weight.
|
The factors include:
|
||||||
|
- Letter count in a file.
|
||||||
|
- Weight of links coming forward and/or backward out of a file.
|
||||||
|
- Whether we should count every descendant coming forward out of a file or just the children.
|
||||||
|
- You can manually add size for certain files too.
|
||||||
|
|
||||||
|
## Demo
|
||||||
<img src="./screenshots/1.jpeg">
|
<img src="./screenshots/1.jpeg">
|
||||||
<img src="./screenshots/2.jpeg">
|
<img src="./screenshots/2.jpeg">
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
1. Search for "Node Factor" in Obsidian's community plugin browser.
|
1. Search for "Node Factor" in Obsidian's community plugin browser.
|
||||||
2. Install it.
|
2. Install it.
|
||||||
|
|
@ -10,13 +17,17 @@ The factors include, letter count, forward & backward weight, and forward tree w
|
||||||
## Configuration Options
|
## Configuration Options
|
||||||
| Option | Meaning | Default value |
|
| Option | Meaning | Default value |
|
||||||
| ------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------- |
|
| ------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------- |
|
||||||
| Forward link weight multiplier | Weight of single link coming out of a node | 1 |
|
| Forward link weight multiplier | Weight of single link coming forward out of a node | 1 |
|
||||||
| Travel forward tree | Determines if forward weight is decided by just the immediate nodes coming out or everything after it | off |
|
| Travel forward tree | Determines if forward weight is decided by just the immediate nodes coming out or everything after it | off |
|
||||||
| Backward link weight multiplier | Weight of single link coming out of a node | 1 |
|
| Backward link weight multiplier | Weight of single link coming backward out of a node | 1 |
|
||||||
| Character per weight | Given number of letters give 1 weight | 0 |
|
| Character per weight | Given number of letters give 1 weight | 0 |
|
||||||
|
| Manually Set Weight | Manually set weight of any file, weight set here overrides everything else | empty set |
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
CalfMoon: [Email](kritagyabhattarai@proton.me), [Github](https://github.com/CalfMoon)
|
CalfMoon: [Email](kritagyabhattarai@proton.me), [Github](https://github.com/CalfMoon)
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
This project still new and might have a few bugs. For bug reports use this repo's Issues section.
|
This project is still new and might have a few bugs. For bug reports use this repo's Issues section.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
OBSD, see [LICENSE](LICENSE).
|
MIT, see [LICENSE](LICENSE).
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import builtins from "builtin-modules";
|
import { builtinModules } from 'node:module';
|
||||||
|
|
||||||
const banner = `/*
|
const banner =
|
||||||
|
`/*
|
||||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
if you want to view the source, please visit the github repository of this plugin
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
*/
|
*/
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const prod = process.argv[2] === "production";
|
const prod = (process.argv[2] === "production");
|
||||||
|
|
||||||
const context = await esbuild.context({
|
const context = await esbuild.context({
|
||||||
banner: {
|
banner: {
|
||||||
|
|
@ -30,8 +31,7 @@ const context = await esbuild.context({
|
||||||
"@lezer/common",
|
"@lezer/common",
|
||||||
"@lezer/highlight",
|
"@lezer/highlight",
|
||||||
"@lezer/lr",
|
"@lezer/lr",
|
||||||
...builtins,
|
...builtinModules],
|
||||||
],
|
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
target: "es2018",
|
target: "es2018",
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
|
|
|
||||||
34
eslint.config.mts
Normal file
34
eslint.config.mts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||||
|
import globals from "globals";
|
||||||
|
import { globalIgnores } from "eslint/config";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
projectService: {
|
||||||
|
allowDefaultProject: [
|
||||||
|
'eslint.config.js',
|
||||||
|
'manifest.json'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
extraFileExtensions: ['.json']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...obsidianmd.configs.recommended,
|
||||||
|
globalIgnores([
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"esbuild.config.mjs",
|
||||||
|
"eslint.config.js",
|
||||||
|
"version-bump.mjs",
|
||||||
|
"versions.json",
|
||||||
|
"main.js",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"id": "node-factor",
|
"id": "node-factor",
|
||||||
"name": "Node Factor",
|
"name": "Node Factor",
|
||||||
"version": "1.0.0",
|
"version": "3.0.0",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Customize factors effecting node size in graph.",
|
"description": "Customize factors effecting node size in graph.",
|
||||||
"author": "CalfMoon",
|
"author": "CalfMoon",
|
||||||
"authorUrl": "https://github.com/CalfMoon",
|
"authorUrl": "https://github.com/CalfMoon",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
}
|
}
|
||||||
4277
package-lock.json
generated
4277
package-lock.json
generated
File diff suppressed because it is too large
Load diff
26
package.json
26
package.json
|
|
@ -1,24 +1,30 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-node-factor",
|
"name": "node-factor",
|
||||||
"version": "1.0.0",
|
"version": "3.0.0",
|
||||||
"description": "Customize factors effecting node size in graph.",
|
"description": "Customize factors effecting node size in graph.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"dev": "node esbuild.config.mjs",
|
||||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||||
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "CalfMoon",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "9.30.1",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
"esbuild": "0.28.1",
|
||||||
"@typescript-eslint/parser": "5.29.0",
|
"eslint": "^9.39.4",
|
||||||
"builtin-modules": "3.3.0",
|
"eslint-plugin-obsidianmd": "0.1.9",
|
||||||
"esbuild": "0.17.3",
|
"globals": "^14.0.0",
|
||||||
"obsidian": "latest",
|
"jiti": "2.6.1",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "4.7.4"
|
"typescript": "^5.8.3",
|
||||||
|
"typescript-eslint": "^8.35.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"obsidian": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
67
src/calculator.ts
Normal file
67
src/calculator.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { App } from "obsidian";
|
||||||
|
|
||||||
|
import { NodeFactorSettings, ObsidianNode } from "types";
|
||||||
|
|
||||||
|
export default class Calculations {
|
||||||
|
private app: App;
|
||||||
|
private settings: NodeFactorSettings;
|
||||||
|
|
||||||
|
constructor(app: App, settings: NodeFactorSettings) {
|
||||||
|
this.app = app;
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcNodeWeight(node: ObsidianNode): number {
|
||||||
|
const settings = this.settings;
|
||||||
|
let weight = 0;
|
||||||
|
|
||||||
|
const manualFileData = settings.manual.find(
|
||||||
|
(manualFileData) => manualFileData.id == node.id,
|
||||||
|
);
|
||||||
|
if (manualFileData != null) {
|
||||||
|
return manualFileData.weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
weight += Object.keys(node.reverse).length * settings.bwdMultiplier;
|
||||||
|
if (settings.fwdTree) {
|
||||||
|
weight +=
|
||||||
|
this.fwdNodeTreeSize(node, new Set()) * settings.fwdMultiplier;
|
||||||
|
} else {
|
||||||
|
weight += Object.keys(node.forward).length * settings.fwdMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.lettersPerWt != 0) {
|
||||||
|
weight += this.letterCount(node) / settings.lettersPerWt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private letterCount(node: ObsidianNode): number {
|
||||||
|
const file = this.app.vault.getFileByPath(node.id);
|
||||||
|
if (file == null || file.extension != "md") return 0;
|
||||||
|
|
||||||
|
return file.stat.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fwdNodeTreeSize(
|
||||||
|
node: ObsidianNode,
|
||||||
|
antiLoopSet: Set<string>,
|
||||||
|
): number {
|
||||||
|
let size = 0;
|
||||||
|
antiLoopSet.add(node.id);
|
||||||
|
|
||||||
|
Object.entries(node.forward).forEach(([key, value]) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const childNode: ObsidianNode = value.target;
|
||||||
|
|
||||||
|
// Prevents looping if A -> B -> C -> D -> B
|
||||||
|
if (!antiLoopSet.has(key)) {
|
||||||
|
size++;
|
||||||
|
const childSize = this.fwdNodeTreeSize(childNode, antiLoopSet);
|
||||||
|
size += childSize;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/main.ts
163
src/main.ts
|
|
@ -1,131 +1,80 @@
|
||||||
import { Plugin } from "obsidian";
|
import { Plugin } from "obsidian";
|
||||||
|
|
||||||
import { ObsidianNode, NodeFactorSettings, DEFAULT_SETTINGS } from "./types";
|
import { ObsidianNode, NodeFactorSettings, DEFAULT_SETTINGS } from "types";
|
||||||
import SampleSettingTab from "./settings";
|
import NodeFactorSettingTab from "settings";
|
||||||
|
import Calculator from "calculator";
|
||||||
|
|
||||||
export default class NodeFactor extends Plugin {
|
export default class NodeFactor extends Plugin {
|
||||||
settings: NodeFactorSettings;
|
settings: NodeFactorSettings;
|
||||||
|
|
||||||
// stops loop when graph isn't open
|
|
||||||
private updateLoop: boolean;
|
|
||||||
|
|
||||||
// This is a hash map that helps in specifically optimizing the forward tree
|
|
||||||
private treeOptimizeMap: Map<string, number> = new Map();
|
|
||||||
|
|
||||||
// This array helps in optimizing the whole calculation process
|
|
||||||
private storedSized: Array<number> = [];
|
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
this.addSettingTab(new NodeFactorSettingTab(this.app, this));
|
||||||
|
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.workspace.on("layout-change", () => {
|
this.app.workspace.on("active-leaf-change", () =>
|
||||||
const leaf = this.app.workspace
|
this.updateGraph(),
|
||||||
.getLeavesOfType("graph")
|
),
|
||||||
.first();
|
);
|
||||||
if (!leaf) {
|
this.registerEvent(
|
||||||
this.updateLoop = false;
|
this.app.workspace.on("layout-change", () => this.updateGraph()),
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updateLoop = true;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const nodes: ObsidianNode[] = leaf.view.renderer.nodes;
|
|
||||||
if (nodes.length === 0) return;
|
|
||||||
|
|
||||||
this.calcLoop(nodes);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// clear cache when there is change in the vault
|
// clear cache when there is change in the vault
|
||||||
this.registerEvent(
|
this.app.workspace.onLayoutReady(() => {
|
||||||
this.app.vault.on("create", () => (this.storedSized = [])),
|
// layout ready is required because obsidian calls
|
||||||
);
|
// create for every existing file when initally loading obsidian
|
||||||
this.registerEvent(
|
// https://docs.obsidian.md/plugins/guides/load-time#Pitfalls
|
||||||
this.app.vault.on("modify", () => (this.storedSized = [])),
|
this.registerEvent(
|
||||||
);
|
this.app.vault.on("create", () => this.recalculateSize()),
|
||||||
this.registerEvent(
|
);
|
||||||
this.app.vault.on("delete", () => (this.storedSized = [])),
|
|
||||||
);
|
|
||||||
this.registerEvent(
|
|
||||||
this.app.vault.on("rename", () => (this.storedSized = [])),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onunload() {
|
|
||||||
this.updateLoop = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private calcLoop(nodes: ObsidianNode[]) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateNodes(nodes);
|
|
||||||
if (this.updateLoop) this.calcLoop(nodes);
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateNodes(nodes: ObsidianNode[]) {
|
|
||||||
this.treeOptimizeMap.clear();
|
|
||||||
nodes.forEach((node, i) => {
|
|
||||||
let weight: number;
|
|
||||||
if (this.storedSized[i] != undefined) {
|
|
||||||
weight = this.storedSized[i];
|
|
||||||
} else {
|
|
||||||
weight = this.calcNodeWeight(node);
|
|
||||||
this.storedSized[i] = weight;
|
|
||||||
}
|
|
||||||
node.weight = weight;
|
|
||||||
});
|
});
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.vault.on("modify", () => this.recalculateSize()),
|
||||||
|
);
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.vault.on("delete", () => this.recalculateSize()),
|
||||||
|
);
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.vault.on("rename", () => this.recalculateSize()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calcNodeWeight(node: ObsidianNode): number {
|
onunload() {}
|
||||||
const settings = this.settings;
|
|
||||||
let weight = 0;
|
|
||||||
|
|
||||||
weight += Object.keys(node.reverse).length * settings.bwdMultiplier;
|
private sizeCache: Map<string, number> = new Map();
|
||||||
if (settings.fwdTree) {
|
private timeoutId: NodeJS.Timeout;
|
||||||
weight +=
|
private updateGraph() {
|
||||||
this.fwdNodeTreeSize(node, node.id) * settings.fwdMultiplier;
|
const leaf = this.app.workspace.getLeavesOfType("graph").first();
|
||||||
} else {
|
// don't run if graph page isn't loaded
|
||||||
weight += Object.keys(node.forward).length * settings.fwdMultiplier;
|
if (!leaf) return;
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.lettersPerWt != 0) {
|
// @ts-ignore
|
||||||
weight += this.letterCount(node) / settings.lettersPerWt;
|
const nodes: Array<ObsidianNode> = leaf.view.renderer.nodes;
|
||||||
}
|
if (nodes.length === 0) return;
|
||||||
|
|
||||||
return Math.round(weight);
|
// Slight delay in calculations is needed to fix node size
|
||||||
|
// if graph view is initially opened when opening obsidian
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
const calculator = new Calculator(this.app, this.settings);
|
||||||
|
|
||||||
|
nodes.forEach((node, _i) => {
|
||||||
|
let weight = 0;
|
||||||
|
if (this.sizeCache.get(node.id) != undefined) {
|
||||||
|
weight = this.sizeCache.get(node.id) as number;
|
||||||
|
} else {
|
||||||
|
weight = calculator.calcNodeWeight(node);
|
||||||
|
this.sizeCache.set(node.id, weight);
|
||||||
|
}
|
||||||
|
node.weight = weight;
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
private letterCount(node: ObsidianNode): number {
|
recalculateSize() {
|
||||||
const file = this.app.vault.getFileByPath(node.id);
|
this.sizeCache.clear();
|
||||||
if (!file || file.extension != "md") return 0;
|
this.updateGraph();
|
||||||
|
|
||||||
return file.stat.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
private fwdNodeTreeSize(node: ObsidianNode, id: string): number {
|
|
||||||
let size = 0;
|
|
||||||
|
|
||||||
const sizeMap = this.treeOptimizeMap.get(node.id);
|
|
||||||
if (sizeMap !== undefined) {
|
|
||||||
return sizeMap as number;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(node.forward).forEach(([key, value]) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const childNode: ObsidianNode = value.target;
|
|
||||||
|
|
||||||
// Prevents looping if A -> B -> C -> A
|
|
||||||
if (key === id) return size;
|
|
||||||
|
|
||||||
size++;
|
|
||||||
const childSize = this.fwdNodeTreeSize(childNode, id);
|
|
||||||
size += childSize;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.treeOptimizeMap.set(node.id, size);
|
|
||||||
return size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
|
||||||
|
|
||||||
import NodeFactor from "./main";
|
|
||||||
|
|
||||||
export default class SampleSettingTab extends PluginSettingTab {
|
|
||||||
plugin: NodeFactor;
|
|
||||||
|
|
||||||
constructor(app: App, plugin: NodeFactor) {
|
|
||||||
super(app, plugin);
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
display(): void {
|
|
||||||
const { containerEl } = this;
|
|
||||||
containerEl.empty();
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Forward link weight multiplier")
|
|
||||||
.setDesc("Multiplier for forward links weight (0 to diable).")
|
|
||||||
.addSlider((slider) =>
|
|
||||||
slider
|
|
||||||
.setLimits(0, 20, 1)
|
|
||||||
.setDynamicTooltip()
|
|
||||||
.setValue(this.plugin.settings.fwdMultiplier)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.fwdMultiplier = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Travel forward tree")
|
|
||||||
.setDesc("Travel forward tree to calcuate node size.")
|
|
||||||
.addToggle((toggle) =>
|
|
||||||
toggle
|
|
||||||
.setValue(this.plugin.settings.fwdTree)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.fwdTree = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Backward link weight multiplier")
|
|
||||||
.setDesc("Multiplier for backward links weight (0 to diable).")
|
|
||||||
.addSlider((slider) =>
|
|
||||||
slider
|
|
||||||
.setLimits(0, 20, 1)
|
|
||||||
.setDynamicTooltip()
|
|
||||||
.setValue(this.plugin.settings.bwdMultiplier)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.bwdMultiplier = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Character per weight")
|
|
||||||
.setDesc(
|
|
||||||
"Add 1 weight to node size per no of given Character (0 to disable).",
|
|
||||||
)
|
|
||||||
.addSlider((slider) =>
|
|
||||||
slider
|
|
||||||
.setLimits(0, 5000, 100)
|
|
||||||
.setDynamicTooltip()
|
|
||||||
.setValue(this.plugin.settings.lettersPerWt)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.lettersPerWt = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
src/settings/file-suggest.ts
Normal file
32
src/settings/file-suggest.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { App, AbstractInputSuggest } from "obsidian";
|
||||||
|
|
||||||
|
export default class FileSuggest extends AbstractInputSuggest<string> {
|
||||||
|
private files: Array<string>;
|
||||||
|
private inputEl: HTMLInputElement;
|
||||||
|
|
||||||
|
constructor(app: App, inputEl: HTMLInputElement) {
|
||||||
|
super(app, inputEl);
|
||||||
|
|
||||||
|
this.inputEl = inputEl;
|
||||||
|
this.files = this.app.vault.getFiles().map((file) => file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSuggestions(inputStr: string): string[] {
|
||||||
|
const inputLower = inputStr.toLowerCase();
|
||||||
|
|
||||||
|
return this.files.filter((file) =>
|
||||||
|
file.toLowerCase().includes(inputLower),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSuggestion(folder: string, el: HTMLElement): void {
|
||||||
|
el.setText(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSuggestion(folder: string): void {
|
||||||
|
this.inputEl.value = folder;
|
||||||
|
const event = new Event("input");
|
||||||
|
this.inputEl.dispatchEvent(event);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/settings/index.ts
Normal file
26
src/settings/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { App, PluginSettingTab } from "obsidian";
|
||||||
|
|
||||||
|
import NodeFactor from "main";
|
||||||
|
|
||||||
|
import ManualSetting from "./manual";
|
||||||
|
import ProgrameticalSetting from "./programetical";
|
||||||
|
|
||||||
|
export default class NodeFactorSettingTab extends PluginSettingTab {
|
||||||
|
private plugin: NodeFactor;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: NodeFactor) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const { plugin, containerEl, app } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
new ProgrameticalSetting(plugin, containerEl).display();
|
||||||
|
|
||||||
|
new ManualSetting(app, plugin, containerEl, () =>
|
||||||
|
this.display(),
|
||||||
|
).display();
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/settings/manual.ts
Normal file
115
src/settings/manual.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import {
|
||||||
|
App,
|
||||||
|
ButtonComponent,
|
||||||
|
Notice,
|
||||||
|
SearchComponent,
|
||||||
|
Setting,
|
||||||
|
SliderComponent,
|
||||||
|
} from "obsidian";
|
||||||
|
|
||||||
|
import NodeFactor from "main";
|
||||||
|
import { FileData } from "types";
|
||||||
|
|
||||||
|
import FileSuggest from "./file-suggest";
|
||||||
|
|
||||||
|
export default class ManualSetting {
|
||||||
|
private app: App;
|
||||||
|
private plugin: NodeFactor;
|
||||||
|
private containerEl: HTMLElement;
|
||||||
|
private refreshDisplay: () => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
app: App,
|
||||||
|
plugin: NodeFactor,
|
||||||
|
containerEl: HTMLElement,
|
||||||
|
refreshDisplay: () => void,
|
||||||
|
) {
|
||||||
|
this.app = app;
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.containerEl = containerEl;
|
||||||
|
this.refreshDisplay = refreshDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
new Setting(this.containerEl).setName("Manually set size").setHeading();
|
||||||
|
|
||||||
|
let selectedWeight: SliderComponent;
|
||||||
|
let selectedFile: SearchComponent;
|
||||||
|
let submitButton: ButtonComponent;
|
||||||
|
|
||||||
|
const addWt = new Setting(this.containerEl)
|
||||||
|
.setName("Add new weight")
|
||||||
|
.setDesc("Weight added here overrides everything else.");
|
||||||
|
|
||||||
|
addWt.addSearch((search) => {
|
||||||
|
new FileSuggest(this.app, search.inputEl);
|
||||||
|
selectedFile = search;
|
||||||
|
search.setPlaceholder("Enter file name");
|
||||||
|
search.onChange(async (value) => {
|
||||||
|
submitButton.setDisabled(value.length === 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
addWt.addSlider((slider) => {
|
||||||
|
selectedWeight = slider;
|
||||||
|
slider.setLimits(0, 100, 5);
|
||||||
|
slider.setDynamicTooltip();
|
||||||
|
slider.setValue(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
addWt.addButton((button) => {
|
||||||
|
submitButton = button;
|
||||||
|
button.setDisabled(true);
|
||||||
|
button.setButtonText("Add");
|
||||||
|
button.setTooltip("Click to add new manual size");
|
||||||
|
|
||||||
|
button.onClick(async () => {
|
||||||
|
const enteredFileData: FileData = {
|
||||||
|
id: selectedFile.getValue(),
|
||||||
|
weight: selectedWeight.getValue(),
|
||||||
|
};
|
||||||
|
selectedWeight.setValue(0);
|
||||||
|
selectedFile.setValue("");
|
||||||
|
|
||||||
|
const fileExists = this.plugin.settings.manual.find(
|
||||||
|
(foundFile) => foundFile.id == enteredFileData.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fileExists != undefined) {
|
||||||
|
new Notice(
|
||||||
|
"That file's size has already been entered," +
|
||||||
|
" remove it first to change weight.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugin.settings.manual.push(enteredFileData);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
new Notice("New size added manually");
|
||||||
|
|
||||||
|
this.refreshDisplay();
|
||||||
|
this.plugin.recalculateSize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Display all manually added weights & option to remove them
|
||||||
|
this.plugin.settings.manual.forEach((value: FileData) => {
|
||||||
|
const manualDisplay = new Setting(this.containerEl)
|
||||||
|
.setName(value.id)
|
||||||
|
.setDesc(`Weight: ${String(value.weight)}`);
|
||||||
|
|
||||||
|
manualDisplay.addButton((button) => {
|
||||||
|
button.setIcon("trash");
|
||||||
|
button.setTooltip("Remove manually added size");
|
||||||
|
|
||||||
|
button.onClick(async () => {
|
||||||
|
this.plugin.settings.manual.remove(value);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
|
||||||
|
this.refreshDisplay();
|
||||||
|
this.plugin.recalculateSize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/settings/programetical.ts
Normal file
85
src/settings/programetical.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Setting } from "obsidian";
|
||||||
|
|
||||||
|
import NodeFactor from "main";
|
||||||
|
|
||||||
|
export default class ProgrameticalSetting {
|
||||||
|
private plugin: NodeFactor;
|
||||||
|
private containerEl: HTMLElement;
|
||||||
|
|
||||||
|
constructor(plugin: NodeFactor, containerEl: HTMLElement) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.containerEl = containerEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
new Setting(this.containerEl)
|
||||||
|
.setName("Programatically set size")
|
||||||
|
.setHeading();
|
||||||
|
|
||||||
|
const fwdLinkWt = new Setting(this.containerEl)
|
||||||
|
.setName("Forward link weight multiplier")
|
||||||
|
.setDesc("Multiplier for forward links weight (0 to disable).");
|
||||||
|
|
||||||
|
fwdLinkWt.addSlider((slider) => {
|
||||||
|
slider.setLimits(0, 20, 1);
|
||||||
|
slider.setDynamicTooltip();
|
||||||
|
slider.setValue(this.plugin.settings.fwdMultiplier);
|
||||||
|
|
||||||
|
slider.onChange(async (value) => {
|
||||||
|
this.plugin.settings.fwdMultiplier = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.plugin.recalculateSize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const travelFwdTree = new Setting(this.containerEl)
|
||||||
|
.setName("Travel forward tree")
|
||||||
|
.setDesc(
|
||||||
|
"Travel forward and add all other nodes to determine final node size.",
|
||||||
|
);
|
||||||
|
|
||||||
|
travelFwdTree.addToggle((toggle) => {
|
||||||
|
toggle.setValue(this.plugin.settings.fwdTree);
|
||||||
|
|
||||||
|
toggle.onChange(async (value) => {
|
||||||
|
this.plugin.settings.fwdTree = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.plugin.recalculateSize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const bwdLinkWt = new Setting(this.containerEl)
|
||||||
|
.setName("Backward link weight multiplier")
|
||||||
|
.setDesc("Multiplier for backward links weight (0 to disable).");
|
||||||
|
|
||||||
|
bwdLinkWt.addSlider((slider) => {
|
||||||
|
slider.setLimits(0, 20, 1);
|
||||||
|
slider.setDynamicTooltip();
|
||||||
|
slider.setValue(this.plugin.settings.bwdMultiplier);
|
||||||
|
|
||||||
|
slider.onChange(async (value) => {
|
||||||
|
this.plugin.settings.bwdMultiplier = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.plugin.recalculateSize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const charWt = new Setting(this.containerEl)
|
||||||
|
.setName("Character per weight")
|
||||||
|
.setDesc(
|
||||||
|
"Add 1 weight to node size per no of given character (0 to disable).",
|
||||||
|
);
|
||||||
|
|
||||||
|
charWt.addSlider((slider) => {
|
||||||
|
slider.setLimits(0, 5000, 100);
|
||||||
|
slider.setDynamicTooltip();
|
||||||
|
slider.setValue(this.plugin.settings.lettersPerWt);
|
||||||
|
|
||||||
|
slider.onChange(async (value) => {
|
||||||
|
this.plugin.settings.lettersPerWt = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.plugin.recalculateSize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,13 @@ export interface NodeFactorSettings {
|
||||||
bwdMultiplier: number;
|
bwdMultiplier: number;
|
||||||
|
|
||||||
lettersPerWt: number;
|
lettersPerWt: number;
|
||||||
|
|
||||||
|
manual: Array<FileData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileData {
|
||||||
|
id: string;
|
||||||
|
weight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: NodeFactorSettings = {
|
export const DEFAULT_SETTINGS: NodeFactorSettings = {
|
||||||
|
|
@ -21,4 +28,6 @@ export const DEFAULT_SETTINGS: NodeFactorSettings = {
|
||||||
bwdMultiplier: 1,
|
bwdMultiplier: 1,
|
||||||
|
|
||||||
lettersPerWt: 0,
|
lettersPerWt: 0,
|
||||||
|
|
||||||
|
manual: new Array<FileData>(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,30 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": "src",
|
||||||
"inlineSourceMap": true,
|
"inlineSourceMap": true,
|
||||||
"inlineSources": true,
|
"inlineSources": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "node",
|
"noImplicitThis": true,
|
||||||
"importHelpers": true,
|
"noImplicitReturns": true,
|
||||||
"isolatedModules": true,
|
"moduleResolution": "node",
|
||||||
"strictNullChecks": true,
|
"importHelpers": true,
|
||||||
"lib": [
|
"noUncheckedIndexedAccess": true,
|
||||||
"DOM",
|
"isolatedModules": true,
|
||||||
"ES5",
|
"strictNullChecks": true,
|
||||||
"ES6",
|
"strictBindCallApply": true,
|
||||||
"ES7"
|
"allowSyntheticDefaultImports": true,
|
||||||
]
|
"useUnknownInCatchVariables": true,
|
||||||
},
|
"lib": [
|
||||||
"include": [
|
"DOM",
|
||||||
"**/*.ts"
|
"ES5",
|
||||||
]
|
"ES6",
|
||||||
|
"ES7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,15 @@ import { readFileSync, writeFileSync } from "fs";
|
||||||
const targetVersion = process.env.npm_package_version;
|
const targetVersion = process.env.npm_package_version;
|
||||||
|
|
||||||
// read minAppVersion from manifest.json and bump version to target version
|
// read minAppVersion from manifest.json and bump version to target version
|
||||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||||
const { minAppVersion } = manifest;
|
const { minAppVersion } = manifest;
|
||||||
manifest.version = targetVersion;
|
manifest.version = targetVersion;
|
||||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||||
|
|
||||||
// update versions.json with target version and minAppVersion from manifest.json
|
// update versions.json with target version and minAppVersion from manifest.json
|
||||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
// but only if the target version is not already in versions.json
|
||||||
versions[targetVersion] = minAppVersion;
|
const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
|
||||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
if (!Object.values(versions).includes(minAppVersion)) {
|
||||||
|
versions[targetVersion] = minAppVersion;
|
||||||
|
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
{
|
{
|
||||||
"1.0.0": "0.15.0"
|
"1.0.0": "0.15.0",
|
||||||
}
|
"2.0.0": "0.15.0",
|
||||||
|
"2.1.0": "0.15.0"
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue