ignore gh-pages files

This commit is contained in:
erhazan 2025-10-13 23:30:10 +02:00
parent a961cfbfd7
commit dd4e2f5469
21 changed files with 100 additions and 350 deletions

View file

@ -8,11 +8,12 @@ on:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

4
.gitignore vendored
View file

@ -20,3 +20,7 @@ main.js
.obsidian
dist
# gh-pages
_site
.tweet-cache

View file

@ -1,6 +1,6 @@
# Obsidian N-brace Plugin
![Obsidian N-brace Plugin](https://github.com/nn-ninja/n-brace)
[Obsidian N-brace Plugin](https://nn-ninja.github.io/n-brace/) Homepage
A more intuitive Graph navigation and visualization for Obsidian.
@ -13,7 +13,7 @@ https://buymeacoffee.com/nbrace
### Through community plugin store
You can install this on community plugin store (not yet).
You can install this on community plugin store.
### Manual installation
@ -22,11 +22,34 @@ You can install this on community plugin store (not yet).
3. `cd obsidian-n-brace && bun install && bun run build`
4. there you go 🎉
## Usage
![graph.png](docs/graph.png)
In a note tab context menu (right click), click on `use N-brace` to open a local graph of viewed note.
![menu.png](docs/menu.png)
The graph appears in the right-hand view and is designed to be used alongside the note on the left. Its purpose is to help you quickly understand how the current note connects to others and to visualize its overall structure.
G-span defines how large a portion of the graph is displayed. When the number of visible nodes reaches this limit, the view automatically shrinks by hiding the most distant nodes.
### Navigation
You can navigate using the keyboard arrow keys. Notes that havent been expanded yet in the local graph are shown with a dotted border.
If you need to explore the parent notes, hold Ctrl to reverse the navigation direction — the link directions will swap accordingly.
## Notes
At present, the n-brace graph model is initialized during application startup, which may result in longer loading times.
I have in my plans to optimize this process.
## Features
### Local Graph
In a note tab context menu (right click), click on `N-brace` to open a local graph of viewed note.
It is designed to rather operate on a local graph, not on huge and unclear global graph.
### Save Setting

View file

@ -1,65 +0,0 @@
## Plugin architecture
The plugin consist of following component:
1. plugin setting manager
1. it controls the settings of the plugin
2. responsible for loading and saving setting
2. global graph view, a wrapper of graph setting manager and force graph
3. local graph view, a wrapper of graph setting manager and force graph
4. graph setting manager
1. each graph view will have a graph setting manager
2. it resposible for the control of the whole graph, then tell the force graph how to respond for setting changes
3. the setting manager can be isolated from force graph because it follow separation of concern principle
5. force graph
1. the graph itself render the the nodes and links
2. interaction manager
1. it is responsible to handle all the interaction of the user and the force graph
6. Graph Command
1. has very simple API to extends commands
```mermaid
classDiagram
class pluginSettingManager {
+ controls the settings of the plugin
+ loads and saves setting
}
class globalGraphView {
}
class localGraphView {
}
class graphSettingManager {
+ controls the whole graph
+ communicates setting changes to force graph
}
class forceGraph {
+ renders nodes and links
}
class interactionManager {
+ handles user interaction with force graph
}
class GraphCommand {
+ extends commands through simple API
}
pluginSettingManager --|> globalGraphView: contains
pluginSettingManager --|> localGraphView: contains
graphSettingManager --|> globalGraphView: contains
graphSettingManager --|> localGraphView: contains
forceGraph --|> globalGraphView: contains
forceGraph --|> localGraphView: contains
interactionManager --|> forceGraph: is part of
GraphCommand --|> globalGraphView: can be used by
GraphCommand --|> localGraphView: can be used by
```
## Key guideline for plugin development
1. Each component here follow separation of concern principle. They can be isolated easily and still functions properly.

BIN
bun.lockb

Binary file not shown.

View file

@ -21,8 +21,7 @@
"showExtension": false,
"showFullPath": false,
"showLinkArrow": true,
"dontMoveWhenDrag": false,
"dagOrientation": "null"
"dontMoveWhenDrag": false
}
},
"pluginSetting": {

BIN
docs/graph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

BIN
docs/menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,11 +1,11 @@
{
"id": "n-brace-plugin",
"id": "n-brace",
"name": "N-brace",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "The N-brace plugin enhances navigation and clarity in the local graph view.",
"minAppVersion": "1.9.14",
"description": "Altered local graph's visual layout. Quick graph navigation.",
"author": "Rafael Hazan",
"authorUrl": "https://hayio.github.io/n-brace/",
"authorUrl": "https://nn-ninja.github.io/n-brace",
"fundingUrl": "https://buymeacoffee.com/nbrace",
"isDesktopOnly": false
}

View file

@ -1,6 +1,6 @@
{
"name": "n-brace",
"version": "1.1.11",
"version": "1.0.0",
"description": "A graph navigation and visualization for Obsidian",
"main": "main.js",
"scripts": {
@ -55,7 +55,7 @@
"react-dom": "18.3.1",
"lucide-react": "^0.511.0",
"jotai": "^2.9.1",
"force-graph": "^1.43.5",
"react-force-graph-2d": "^1.29.0",
"@tweenjs/tween.js": "^21.0.0",
"copy-anything": "^3.0.5",
"d3": "^7.9.0",

View file

@ -63,4 +63,4 @@ echo "Created tag $NEW_VERSION"
git push origin "$NEW_VERSION"
echo "Pushed tag $NEW_VERSION to the origin branch $NEW_VERSION"
git push
echo "Pushed to the origin master branch"
echo "Pushed to the origin main branch"

View file

@ -1,18 +1,7 @@
import type { ISettingManager } from "@/Interfaces";
import { AsyncQueue } from "@/util/AsyncQueue";
import type {
GraphSetting,
LocalGraphSettings,
MarkdownPostProcessorGraphSettings,
Setting,
} from "@/SettingsSchemas";
import {
defaultLocalGraphSetting,
defaultMarkdownPostProcessorGraphSetting,
GraphType,
SearchEngineType,
SettingSchema,
} from "@/SettingsSchemas";
import type { Setting } from "@/SettingsSchemas";
import { defaultLocalGraphSetting, SearchEngineType, SettingSchema } from "@/SettingsSchemas";
import { createNotice } from "@/util/createNotice";
import { State } from "@/util/State";
import type { Plugin } from "obsidian";
@ -105,17 +94,6 @@ export class PluginSettingManager implements ISettingManager<Setting> {
}
await this.plugin.saveData(this.setting.value);
}
static getNewSetting(type: GraphType.local): LocalGraphSettings;
static getNewSetting(type: GraphType.postProcessor): MarkdownPostProcessorGraphSettings;
static getNewSetting(type: GraphType): GraphSetting;
static getNewSetting(type: GraphType) {
if (type === GraphType.local) {
return defaultLocalGraphSetting;
} else {
return defaultMarkdownPostProcessorGraphSetting;
}
}
}
export const DEFAULT_SETTING: Setting = {

View file

@ -49,23 +49,6 @@ export enum SearchEngineType {
builtIn = "builtIn",
}
export enum DagOrientation {
td = "td",
bu = "bu",
lr = "lr",
rl = "rl",
zout = "zout",
zin = "zin",
radialout = "radialout",
radialin = "radialin",
null = "null",
}
export enum CommandClickNodeAction {
openNodeInNewTab = "openNodeInNewTab",
focusNode = "focusNode",
}
const commonSetting = {
display: {
nodeSize: nodeSize.default,
@ -101,7 +84,6 @@ export const BaseDisplaySettingsSchema = z.object({
showFullPath: z.boolean().default(commonSetting.display.showFullPath),
showLinkArrow: z.boolean().default(commonSetting.display.showLinkArrow),
dontMoveWhenDrag: z.boolean().default(commonSetting.display.dontMoveWhenDrag),
dagOrientation: z.undefined().or(z.nativeEnum(DagOrientation)).default(DagOrientation.null),
});
export const LocalDisplaySettingsSchema = z.object({
@ -189,24 +171,12 @@ export const SettingSchema = z.object({
}),
});
export type BaseFilterSettings = Prettify<z.TypeOf<typeof BaseFilterSettingsSchema>>;
export type LocalFilterSetting = Prettify<z.TypeOf<typeof LocalFilterSettingSchema>>;
export type GroupSettings = Prettify<z.TypeOf<typeof GroupSettingsSchema>>;
export type BaseDisplaySettings = Prettify<z.TypeOf<typeof BaseDisplaySettingsSchema>>;
export type LocalDisplaySettings = Prettify<z.TypeOf<typeof LocalDisplaySettingsSchema>>;
export type LocalGraphSettings = Prettify<z.TypeOf<typeof LocalGraphSettingsSchema>>;
export type MarkdownPostProcessorGraphSettings = Prettify<
z.TypeOf<typeof MarkdownPostProcessorGraphSettingSchema>
>;
export type SavedSetting = Prettify<z.TypeOf<typeof SavedSettingSchema>>;
export type Setting = Prettify<z.TypeOf<typeof SettingSchema>>;
export type GraphSetting = Exclude<
@ -225,7 +195,6 @@ export const defaultLocalGraphSetting: LocalGraphSettings = {
groups: [],
display: {
...commonSetting.display,
dagOrientation: DagOrientation.null,
},
};
@ -240,6 +209,5 @@ export const defaultMarkdownPostProcessorGraphSetting: MarkdownPostProcessorGrap
groups: [],
display: {
...commonSetting.display,
dagOrientation: DagOrientation.null,
},
};

View file

@ -4,6 +4,6 @@ export const config = {
local: "n-brace-view",
},
displayText: {
local: "N-brace your brain",
local: "N-brace view",
},
};

View file

@ -1,8 +1,7 @@
import type { LinkCache } from "@/graph/Link";
import { Link } from "@/graph/Link";
import { Node } from "@/graph/Node";
import { copy } from "copy-anything";
import type { App, TAbstractFile } from "obsidian";
import type { App } from "obsidian";
export class Graph {
public rootPath: string | undefined;
@ -31,14 +30,6 @@ export class Graph {
this.nodes.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
}
public getNodeByPath = (path: string): Node | null => {
const index = this.nodeIndex.get(path);
if (typeof index === "number") {
return this.nodes[index] ?? null;
}
return null;
};
public getNodeById(id: string): Node | null {
const index = this.nodeIndex.get(id);
if (typeof index === "number") {
@ -47,19 +38,6 @@ export class Graph {
return null;
}
// Returns a link by its source and target node ids
public getLinkByIds(sourceNodeId: string, targetNodeId: string): Link | null {
const sourceLinkMap = this.linkIndex.get(sourceNodeId);
if (sourceLinkMap) {
const index = sourceLinkMap.get(targetNodeId);
if (index !== undefined) {
// @ts-ignore
return this.links[index];
}
}
return null;
}
public getLinksFromNode(sourceNodeId: string): Link[] {
const linkIndexes = this.linkIndex.get(sourceNodeId);
if (linkIndexes) {
@ -70,41 +48,6 @@ export class Graph {
return [];
}
/**
* given a node id, return all the links that contains this node
*/
public getLinksWithNode(nodeId: string): Link[] {
const links: Link[] = [];
this.linkIndex.forEach((targetMap, sourceId) => {
if (sourceId === nodeId) {
links.push(
// @ts-ignore
...Array.from(targetMap.values()).map((index) => this.links[index])
);
} else if (targetMap.has(nodeId)) {
const index = targetMap.get(nodeId);
if (typeof index === "number") {
// @ts-ignore
links.push(this.links[index]);
}
}
});
return links;
}
// Clones the graph
public clone = (): Graph => {
return new Graph(
this.rootPath,
copy(this.nodes),
copy(this.links),
copy(this.nodeIndex),
copy(this.linkIndex)
);
};
public static createEmpty = (): Graph => {
return new Graph(undefined, [], [], new Map(), new Map());
};
@ -231,64 +174,6 @@ export class Graph {
this.nodes.forEach(fun);
};
public static compare = (graph1: Graph, graph2: Graph): boolean => {
// Quick checks for lengths
if (graph1.nodes.length !== graph2.nodes.length) {
return false;
}
if (graph1.links.length !== graph2.links.length) {
return false;
}
// Compare nodes
for (const node1 of graph1.nodes) {
const node2Index = graph2.nodeIndex.get(node1.id);
// @ts-ignore
if (node2Index === undefined || graph2.nodes[node2Index].id !== node1.id) {
return false;
}
}
// Compare links
for (const link1 of graph1.links) {
const graph2SourceLinkMap = graph2.linkIndex.get(link1.source.id);
if (!graph2SourceLinkMap) {
return false;
}
const graph2LinkIndex = graph2SourceLinkMap.get(link1.target.id);
if (graph2LinkIndex === undefined) {
return false;
}
const link2 = graph2.links[graph2LinkIndex];
if (!link2 || link2.source.id !== link1.source.id || link2.target.id !== link1.target.id) {
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);
}
public isAcyclic = (): boolean => {
const visited = new Set<string>();
const recStack = new Set<string>();
for (const node of this.nodes) {
if (this.isCyclicUtil(node.id, visited, recStack)) {
return false;
}
}
return true;
};
private isCyclicUtil = (nodeId: string, visited: Set<string>, recStack: Set<string>): boolean => {
if (!visited.has(nodeId)) {
// Add to visited and recursion stack

View file

@ -114,7 +114,6 @@ export default class ForceGraphPlugin extends Plugin implements HoverParent {
// unregister the resolved cache listener
this.app.metadataCache.off("resolved", this.onGraphCacheReady);
this.app.metadataCache.off("resolve", this.onGraphCacheChanged);
this.app.workspace.detachLeavesOfType(VIEW_TYPE_REACT_FORCE_GRAPH);
}
private initListeners() {
@ -129,7 +128,7 @@ export default class ForceGraphPlugin extends Plugin implements HoverParent {
if (!file) return;
menu.addItem((item) => {
item
.setTitle("N-brace")
.setTitle("use N-brace")
.setIcon(config.icon)
.onClick(() => this.openGraph());
});
@ -146,22 +145,6 @@ export default class ForceGraphPlugin extends Plugin implements HoverParent {
if (!file || !(file instanceof TFile)) {
return;
}
// if (markdownLeaves.length === 0) {
// const rootLeaf = workspace.getMostRecentLeaf() || workspace.getLeaf();
// const newLeaf = workspace.createLeafBySplit(rootLeaf, "vertical", true); // Split left
// await newLeaf.openFile(file);
// workspace.revealLeaf(newLeaf);
// return;
// }
//
// // Use the first existing Markdown leaf
// const targetLeaf = markdownLeaves[markdownLeaves.length - 1];
// if (file) {
// await targetLeaf.openFile(file as any); // Open the file in the leaf
// workspace.revealLeaf(targetLeaf); // Ensure its visible
// } else {
// console.error(`File not found: ${filePath}`);
// }
const graphLeaf = workspace.getLeavesOfType(VIEW_TYPE_REACT_FORCE_GRAPH)[0];
if (!graphLeaf) {

View file

@ -22,7 +22,6 @@ export class ReactForceGraphView extends ItemView {
readonly plugin: ForceGraphPlugin;
readonly store = getDefaultStore();
root: Root | null = null;
private width: number = this.contentEl.offsetWidth;
private settingManager: PluginSettingManager;
constructor(leaf: WorkspaceLeaf, plugin: ForceGraphPlugin) {
@ -85,7 +84,7 @@ export class ReactForceGraphView extends ItemView {
console.debug("node to expand ", nodePath);
if (!nodePath || !this.plugin.globalGraph) {
console.debug("Returning empty graph.");
return Promise.resolve(Graph.createEmpty());
return Graph.createEmpty();
}
const graph = getNewLocalGraph(this.plugin, {
centerFilePath: nodePath,

View file

@ -37,11 +37,11 @@ export class SettingTab extends PluginSettingTab {
containerEl.empty();
containerEl.addClasses(["n-brace-setting-tab"]);
containerEl.createEl("h2", { text: "Settings" });
new Setting(containerEl)
.setName("Base Folder")
.setDesc("Base folder, within which the mind map can navigate.")
.setDesc(
"The base directory where your notes for exploration live — navigation builds from here."
)
.addText((text) => {
text
.setPlaceholder(`${DEFAULT_BASE_FOLDER}`)
@ -68,35 +68,6 @@ export class SettingTab extends PluginSettingTab {
text.inputEl.setAttribute("type", "string");
});
new Setting(containerEl)
.setName("Note title font size")
.setDesc("The font size of the title displayed on a graph node.")
.addText((text) => {
text
.setPlaceholder(`${DEFAULT_TITLE_FONT_SIZE}`)
.setValue(String(pluginSetting.titleFontSize ?? DEFAULT_TITLE_FONT_SIZE))
.onChange(async (value) => {
// check if value is a number
if (isNaN(Number(value)) || Number(value) <= 3) {
// set the error to the input
text.inputEl.setCustomValidity("Please enter a number higher than 3");
this.plugin.settingManager.updateSettings((setting) => {
setting.value.pluginSetting.titleFontSize = DEFAULT_TITLE_FONT_SIZE;
});
} else {
text.inputEl.setCustomValidity("");
this.plugin.settingManager.updateSettings((setting) => {
setting.value.pluginSetting.titleFontSize = Number(value);
eventBus.trigger("settings-updated");
});
}
text.inputEl.reportValidity();
});
text.inputEl.setAttribute("type", "number");
text.inputEl.setAttribute("min", "4");
return text;
});
new Setting(containerEl)
.setName("Default display graph span")
.setDesc("Size of the graph that is displayed at once.")
@ -126,6 +97,37 @@ export class SettingTab extends PluginSettingTab {
return text;
});
new Setting(containerEl).setName("Appearance").setHeading();
new Setting(containerEl)
.setName("Note title font size")
.setDesc("The font size of the title displayed on a graph node.")
.addText((text) => {
text
.setPlaceholder(`${DEFAULT_TITLE_FONT_SIZE}`)
.setValue(String(pluginSetting.titleFontSize ?? DEFAULT_TITLE_FONT_SIZE))
.onChange(async (value) => {
// check if value is a number
if (isNaN(Number(value)) || Number(value) <= 3) {
// set the error to the input
text.inputEl.setCustomValidity("Please enter a number higher than 3");
this.plugin.settingManager.updateSettings((setting) => {
setting.value.pluginSetting.titleFontSize = DEFAULT_TITLE_FONT_SIZE;
});
} else {
text.inputEl.setCustomValidity("");
this.plugin.settingManager.updateSettings((setting) => {
setting.value.pluginSetting.titleFontSize = Number(value);
eventBus.trigger("settings-updated");
});
}
text.inputEl.reportValidity();
});
text.inputEl.setAttribute("type", "number");
text.inputEl.setAttribute("min", "4");
return text;
});
new Setting(containerEl)
.setName("Link Color")
.setDesc("Choose predefined colors or a custom color for graph links")
@ -157,7 +159,7 @@ export class SettingTab extends PluginSettingTab {
setting.value.pluginSetting.linkColorTheme = value;
});
}
this.display();
await this.display();
})
)
.addColorPicker((color) => {

View file

@ -1,9 +1,15 @@
import React, { useEffect, useState } from "react";
import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from "react-icons/fa";
import {
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
FlipVertical,
FlipVertical2,
Sun,
Sunrise,
} from "lucide-react";
import "@/views/graph/GraphControls.css";
import { RiFlipVerticalFill, RiFlipVerticalLine } from "react-icons/ri";
import { PiSunHorizonBold } from "react-icons/pi";
import { IoSunny } from "react-icons/io5";
import { useAtomValue } from "jotai/react";
import { graphSettingsAtom } from "@/atoms/graphAtoms";
@ -73,24 +79,24 @@ export const GraphControls: React.FC<GraphControlsProps> = ({
)}
<div className="pm-graph-controls">
<button onClick={handleToggle} title="Flip direction (Ctrl)">
{isDescending ? <RiFlipVerticalLine /> : <RiFlipVerticalFill />}
{isDescending ? <FlipVertical /> : <FlipVertical2 />}
</button>
<button onClick={handleHidden} title={isHidden ? "Show" : "Hide"}>
{isHidden ? <IoSunny /> : <PiSunHorizonBold />}
{isHidden ? <Sun /> : <Sunrise />}
</button>
{!isHidden && (
<>
<button onClick={onPanUp} title="Descend">
<FaArrowUp />
<ArrowUp />
</button>
<button onClick={onPanDown} title="Go Back">
<FaArrowDown />
<ArrowDown />
</button>
<button onClick={onPanLeft} title="Anti Clockwise">
<FaArrowLeft />
<ArrowLeft />
</button>
<button onClick={onPanRight} title="Clockwise">
<FaArrowRight />
<ArrowRight />
</button>
</>
)}

View file

@ -87,7 +87,7 @@ export const ReactForceGraph: React.FC<GraphComponentProps> = ({
// directional forces depending on if a screen is vertical or horizontal
useEffect(() => {
const fg = fgRef.current;
// TODO optimize: change forces only if dimensions changed a lot
// TODO optimize: change forces only if dimensions changed many times
if (fg) {
console.debug("Setting Graph forces");
// fg.d3Force("link", d3.forceLink()
@ -262,9 +262,6 @@ export const ReactForceGraph: React.FC<GraphComponentProps> = ({
} else {
const nodeDuplicates = new Set<string>();
newLinks = links.filter((l) => {
// const exists = graphData.links.find((existingLink) => {
// return Link.compare(l, existingLink);
// });
const exists = graphData.links.find((existingLink) => {
return (
(existingLink.source.idx === expandedNode.idx ||
@ -303,40 +300,12 @@ export const ReactForceGraph: React.FC<GraphComponentProps> = ({
return false;
}
return !nodeDuplicates.has(n.path);
// const isItParentOfExpanded = n.links.find((l) => l.target.path === expandedNode.path);
// if (isItParentOfExpanded) {
// return !graphData.nodes.find((existingNode) => {
// const exists = Node.compare(n, existingNode);
// // const hasLink = exists && existingNode.links.find((l) => l.tar)
// if (exists) {
// links.forEach((l) => {
// if (l.source.path === existingNode.path) {
// l.source = existingNode;
// } else if (l.target.path === existingNode.path) {
// l.target = existingNode;
// }
// });
// existingNode.links = [
// ...existingNode.links,
// ...n.links.filter(
// (l) =>
// !existingNode.links.find(
// (existingLink) =>
// l.source.path === existingLink.source.path &&
// l.target.path === existingLink.target.path
// )
// ),
// ];
// }
// return exists;
// });
// }
// return true;
});
}
graphData.nodes.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
//// debug code
// const simplifiedNodes = [...graphData.nodes, ...newNodes].map(node => ({
// path: node.path,
// links: node.links.map((link) => link.source.path + "->" + link.target.path),
@ -364,7 +333,6 @@ export const ReactForceGraph: React.FC<GraphComponentProps> = ({
graphData.nodes.find((n) => n.path === selectedNode.selectedPath) as Node,
node
);
// console.info(`HISTORY: ${JSON.stringify(newHistory)}`);
setNavHistory(newHistory);
if (selectedNode.selectedIndex !== undefined) {
const newIndexHistory = stackOnIndexHistory(

View file

@ -51,4 +51,3 @@
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/*# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3JjL3ZpZXdzL2dyYXBoL0dyYXBoQ29udHJvbHMuY3NzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyIucG0tZ3JhcGgtY29udHJvbHMge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogNTBweDtcbiAgcmlnaHQ6IDEwcHg7XG4gIGRpc3BsYXk6IGdyaWQ7XG4gIGdyaWQtdGVtcGxhdGUtY29sdW1uczogMWZyIDFmcjsgLyogMiBjb2x1bW5zICovXG4gIGdhcDogNXB4O1xuICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuOCk7IC8qIFNlbWktdHJhbnNwYXJlbnQgYmFja2dyb3VuZCAqL1xuICBwYWRkaW5nOiA1cHg7XG4gIGJvcmRlci1yYWRpdXM6IDVweDtcbiAgYm94LXNoYWRvdzogMCAycHggNHB4IHJnYmEoMCwgMCwgMCwgMC4yKTtcbn1cblxuLnBtLWdyYXBoLWNvbnRyb2xzIGJ1dHRvbiB7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICBiYWNrZ3JvdW5kOiB2YXIoLS1jb2xvci1iYXNlLTQwKTtcbiAgYm9yZGVyOiBub25lO1xuICBib3JkZXItcmFkaXVzOiA0cHg7XG4gIGN1cnNvcjogcG9pbnRlcjtcbiAgZm9udC1zaXplOiAxNnB4O1xufVxuXG4ucG0tZ3JhcGgtY29udHJvbHMgYnV0dG9uOmhvdmVyIHtcbiAgYmFja2dyb3VuZDogdmFyKC0tY29sb3ItYmFzZS02MCk7XG59XG5cbi5zbGlkZXItY29udHJvbCB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiA1MHB4O1xuICBsZWZ0OiAxMHB4O1xuICBkaXNwbGF5OiBncmlkO1xuICBncmlkLXRlbXBsYXRlLWNvbHVtbnM6IDFmcjtcbiAgZ2FwOiA1cHg7XG4gIGJhY2tncm91bmQ6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC44KTsgLyogU2VtaS10cmFuc3BhcmVudCBiYWNrZ3JvdW5kICovXG4gIHBhZGRpbmc6IDVweDtcbiAgYm9yZGVyLXJhZGl1czogNXB4O1xuICBib3gtc2hhZG93OiAwIDJweCA0cHggcmdiYSgwLCAwLCAwLCAwLjIpO1xuICBjb2xvcjogdmFyKC0tY29sb3ItYmFzZS0wMCk7XG59XG5cbi5zbGlkZXItY29udHJvbCBpbnB1dFt0eXBlPVwicmFuZ2VcIl0ge1xuICB3aWR0aDogMTAwcHg7XG4gIGN1cnNvcjogcG9pbnRlcjtcbn1cblxuLnNsaWRlci12YWx1ZSB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiAtMjBweDsgLyogQWJvdmUgdGhlIHNsaWRlciAqL1xuICBsZWZ0OiA1MCU7XG4gIHRyYW5zZm9ybTogdHJhbnNsYXRlWCgtNTAlKTtcbiAgZm9udC1zaXplOiAxMnB4O1xuICBwYWRkaW5nOiAycHggNXB4O1xuICBib3JkZXItcmFkaXVzOiAzcHg7XG4gIGJveC1zaGFkb3c6IDAgMXB4IDJweCByZ2JhKDAsIDAsIDAsIDAuMSk7XG59XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQUE7QUFDRTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBR0Y7QUFDRTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBQUE7QUFHRjtBQUNFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUdGO0FBQ0U7QUFDQTtBQUFBO0FBR0Y7QUFDRTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7IiwKICAibmFtZXMiOiBbXQp9Cg== */