Implement plugin skeleton

This commit is contained in:
Vadim Beskrovnov 2022-12-09 23:08:03 +00:00
parent 2693b8cd4c
commit 72e9099ab9
17 changed files with 4028 additions and 78 deletions

View file

@ -6,5 +6,5 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4
indent_size = 2
tab_width = 2

View file

@ -1,23 +1,30 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
"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": 1,
"@typescript-eslint/no-unused-vars": [
"error",
{
"args": "none"
}
],
"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"
}
}
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

View file

@ -1,42 +1,44 @@
import esbuild from "esbuild";
import process from "process";
import builtins from 'builtin-modules'
import builtins from "builtin-modules";
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
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";
esbuild.build({
banner: {
js: banner,
},
entryPoints: ['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',
watch: !prod,
target: 'es2018',
logLevel: "info",
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
}).catch(() => process.exit(1));
esbuild
.build({
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",
watch: !prod,
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
})
.catch(() => process.exit(1));

View file

@ -1,11 +1,10 @@
{
"id": "obsidian-sample-plugin",
"name": "Sample Plugin",
"id": "obsidian-contacts",
"name": "Contacts",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"isDesktopOnly": false
}
"description": "Allows you to manage and organize your contacts.",
"author": "Vadim Beskrovnov",
"authorUrl": "https://github.com/vbeskrovnov",
"isDesktopOnly": true
}

3752
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "obsidian-sample-plugin",
"name": "obsidian-contacts",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"description": "Allows you to manage and organize your contacts.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -13,6 +13,8 @@
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
@ -20,5 +22,9 @@
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

5
src/context/context.ts Normal file
View file

@ -0,0 +1,5 @@
import { App } from 'obsidian';
import * as React from "react";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
export const AppContext = React.createContext<App>(undefined!);

7
src/context/hooks.ts Normal file
View file

@ -0,0 +1,7 @@
import { App } from "obsidian";
import * as React from "react";
import { AppContext } from "./context";
export const useApp = (): App => {
return React.useContext(AppContext);
};

7
src/file/file.ts Normal file
View file

@ -0,0 +1,7 @@
import { TFile } from "obsidian";
export async function openFile(file: TFile) {
const { workspace } = window.app;
const leaf = workspace.getLeaf()
await leaf.openFile(file, { active: true });
}

View file

@ -1,4 +1,6 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { App, Editor, MarkdownView, Modal, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { ContactsView } from "src/ui/sidebar/sidebarView";
import { CONTACTS_VIEW_CONFIG } from "src/util/constants";
// Remember to rename these classes and interfaces!
@ -15,11 +17,15 @@ export default class MyPlugin extends Plugin {
async onload() {
await this.loadSettings();
this.registerView(
CONTACTS_VIEW_CONFIG.type,
(leaf) => new ContactsView(leaf)
);
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
const ribbonIconEl = this.addRibbonIcon('contact', 'Contacts', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
this.activateSidebarView();
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
@ -71,7 +77,6 @@ export default class MyPlugin extends Plugin {
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
@ -79,7 +84,7 @@ export default class MyPlugin extends Plugin {
}
onunload() {
this.app.workspace.detachLeavesOfType(CONTACTS_VIEW_CONFIG.type);
}
async loadSettings() {
@ -89,6 +94,19 @@ export default class MyPlugin extends Plugin {
async saveSettings() {
await this.saveData(this.settings);
}
async activateSidebarView() {
this.app.workspace.detachLeavesOfType(CONTACTS_VIEW_CONFIG.type);
await this.app.workspace.getRightLeaf(false).setViewState({
type: CONTACTS_VIEW_CONFIG.type,
active: true,
});
this.app.workspace.revealLeaf(
this.app.workspace.getLeavesOfType(CONTACTS_VIEW_CONFIG.type)[0]
);
}
}
class SampleModal extends Modal {
@ -97,12 +115,12 @@ class SampleModal extends Modal {
}
onOpen() {
const {contentEl} = this;
const { contentEl } = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
const { contentEl } = this;
contentEl.empty();
}
}
@ -116,11 +134,11 @@ class SampleSettingTab extends PluginSettingTab {
}
display(): void {
const {containerEl} = this;
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
containerEl.createEl('h2', { text: 'Settings for my awesome plugin.' });
new Setting(containerEl)
.setName('Setting #1')

8
src/parse/contact.ts Normal file
View file

@ -0,0 +1,8 @@
import { TFile } from "obsidian";
export type Contact = {
name: string;
lastName: string;
phone: string;
file: TFile;
}

36
src/parse/parse.ts Normal file
View file

@ -0,0 +1,36 @@
import { TFile } from "obsidian";
import { Contact } from "./contact";
export async function parseContactData(file: TFile): Promise<Contact | null> {
const { vault } = window.app;
const fileContents = await vault.cachedRead(file);
if (!isContactFile(fileContents)) {
return null;
}
const regexpNames = /^\|(?<key>.+)\|(?<value>.+)\|$/gm;
const contactsDict: { [key: string]: string } = {};
for (const match of fileContents.matchAll(regexpNames)) {
if (!match.groups) {
continue;
}
const key = match.groups.key.trim()
const value = match.groups.value.trim()
if (key === "" || value === "") {
continue;
}
contactsDict[key] = value;
}
return {
name: contactsDict['Name'],
lastName: contactsDict['Last Name'],
phone: contactsDict['Phone'],
file: file,
}
}
function isContactFile(
content: string,
): boolean {
return (content.match(/\/---contact---\//g) || []).length === 2;
}

View file

@ -0,0 +1,18 @@
import * as React from "react";
import { openFile } from "src/file/file";
import { Contact } from "src/parse/contact";
type ContactProps = {
contact: Contact;
};
export const ContactView = (props: ContactProps) => {
const contact = props.contact;
return (
<div>
<p onClick={() => openFile(contact.file)}>
{contact.name} {contact.lastName}
</p>
</div>
);
};

View file

@ -0,0 +1,41 @@
import { randomUUID } from "crypto";
import { normalizePath, TFile, TFolder, Vault } from "obsidian";
import * as React from "react";
import { useApp } from "src/context/hooks";
import { Contact } from "src/parse/contact";
import { parseContactData } from "src/parse/parse";
import { ContactView } from "./ContactView";
export const SidebarRootView = () => {
const { vault } = useApp();
const [contacts, setContacts] = React.useState<Contact[]>([]);
const folder = "03 - Личное/Contacts";
React.useEffect(() => {
const contactsFolder = vault.getAbstractFileByPath(
normalizePath(folder)
) as TFolder;
if (!contactsFolder) {
throw new Error("Failed to find contacts folder");
}
const contactsData: Contact[] = [];
Vault.recurseChildren(contactsFolder, async (contactNote) => {
if (contactNote instanceof TFile) {
const contact = await parseContactData(contactNote);
if (contact) {
contactsData.push(contact);
}
}
});
setContacts(contactsData);
}, []);
return (
<div>
{contacts.map((contact) => (
<ContactView contact={contact} key={randomUUID()} />
))}
</div>
);
};

View file

@ -0,0 +1,37 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import * as React from "react";
import { createRoot } from "react-dom/client";
import { AppContext } from "src/context/context";
import { CONTACTS_VIEW_CONFIG } from "src/util/constants";
import { SidebarRootView } from "./components/SidebarRootView";
export class ContactsView extends ItemView {
root = createRoot(this.containerEl.children[1]);
constructor(leaf: WorkspaceLeaf) {
super(leaf);
}
getViewType(): string {
return CONTACTS_VIEW_CONFIG.type;
}
getDisplayText(): string {
return CONTACTS_VIEW_CONFIG.name;
}
getIcon(): string {
return CONTACTS_VIEW_CONFIG.icon;
}
async onOpen() {
this.root.render(
<AppContext.Provider value={this.app}>
<SidebarRootView />
</AppContext.Provider>
);
}
async onClose() {
this.root.unmount();
}
}

5
src/util/constants.ts Normal file
View file

@ -0,0 +1,5 @@
export const CONTACTS_VIEW_CONFIG = {
type: "contacts-view",
name: "Contacts",
icon: "contact",
};

View file

@ -10,7 +10,8 @@
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"strictNullChecks": true,
"jsx": "react",
"lib": [
"DOM",
"ES5",
@ -19,6 +20,7 @@
]
},
"include": [
"**/*.ts"
"**/*.ts",
"**/*.tsx"
]
}
}