scaffolding

This commit is contained in:
mProjectsCode 2022-05-04 17:45:42 +02:00
parent 2ed775bdd4
commit ddcc767573
13 changed files with 3717 additions and 151 deletions

View file

@ -15,7 +15,7 @@ esbuild.build({
banner: {
js: banner,
},
entryPoints: ['main.ts'],
entryPoints: ['src/main.ts'],
bundle: true,
external: [
'obsidian',

137
main.ts
View file

@ -1,137 +0,0 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
async onload() {
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// 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.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
console.log('Secret: ' + value);
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}

View file

@ -1,10 +1,10 @@
{
"id": "obsidian-sample-plugin",
"name": "Sample Plugin",
"version": "1.0.1",
"id": "obsidian-media-db-plugin",
"name": "Media DB Plugin",
"version": "0.1.0",
"minAppVersion": "0.12.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",
"description": "Coming soon TM",
"author": "Moritz Jung",
"authorUrl": "https://mprojectscode.github.io/",
"isDesktopOnly": false
}

3292
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",
"version": "1.0.1",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"name": "obsidian-media-db-plugin",
"version": "0.1.0",
"description": "Coming soon TM",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -12,6 +12,7 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@popperjs/core": "^2.11.5",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^5.2.0",

6
src/api/APIResult.ts Normal file
View file

@ -0,0 +1,6 @@
export interface APIResult {
title: string,
type: string,
description?: string,
data: any,
}

50
src/main.ts Normal file
View file

@ -0,0 +1,50 @@
import {Plugin} from 'obsidian';
import {DEFAULT_SETTINGS, MediaDbPluginSettings, MediaDbSettingTab} from './settings/settings';
import {MediaDbSearchModal} from './modals/MediaDbSearchModal';
export default class MediaDbPlugin extends Plugin {
settings: MediaDbPluginSettings;
async onload() {
await this.loadSettings();
// add icon to the left ribbon
const ribbonIconEl = this.addRibbonIcon('book', 'Add new Media DB entry', (evt: MouseEvent) =>
this.createMediaDbNote(),
);
ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class');
// register command to open search modal
this.addCommand({
id: 'open-media-db-search-modal',
name: 'Add new Media DB entry',
callback: () => this.createMediaDbNote(),
});
// register the settings tab
this.addSettingTab(new MediaDbSettingTab(this.app, this));
}
async createMediaDbNote(): Promise<void> {
const data = await this.openMediaDbSearchModal();
console.log('Create new note or something...');
console.log(data);
}
async openMediaDbSearchModal(): Promise<any> {
return new Promise(((resolve, reject) => {
new MediaDbSearchModal(this.app, (err, result) => {
if (err) return reject(err);
resolve(result);
}).open();
}));
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}

View file

@ -0,0 +1,96 @@
import {App, ButtonComponent, SuggestModal} from 'obsidian';
import {APIResult} from '../api/APIResult';
import {sleep} from '../utils/utils';
export class MediaDbSearchModal extends SuggestModal<APIResult> {
query: string;
hasChanged: boolean;
isBusy: boolean;
okBtn: ButtonComponent;
onChoose: (err: Error, result?: APIResult) => void;
constructor(app: App, onChoose?: (err: Error, result?: APIResult) => void) {
super(app);
this.onChoose = onChoose;
this.isBusy = false;
}
async search(force: boolean = false): Promise<APIResult[]> {
if (!this.query) {
return;
}
if (!this.isBusy || force) {
this.isBusy = true;
const thisQuery: string = this.query;
console.log('query started with ' + thisQuery)
await sleep(1000); // TODO: replace with real api call
if (this.query === thisQuery) {
this.isBusy = false;
return [ // TODO: replace with real api result
{title: thisQuery, type: 'movie', data: null} as APIResult,
{title: 'test2', type: 'series', data: {episodes: 24}} as APIResult,
];
} else {
return await this.search(true);
}
}
}
async getSuggestions(query: string): Promise<APIResult[]> {
this.query = query;
return await this.search();
}
renderSuggestion(item: APIResult, el: HTMLElement): void {
el.createEl('div', {text: item.title});
el.createEl('small', {text: item.type});
}
onChooseSuggestion(item: APIResult, evt: MouseEvent | KeyboardEvent): void {
this.onChoose(null, item);
}
/*
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Search Media DB' });
const placeholder = 'Search by title';
const textComponent = new TextComponent(contentEl);
textComponent.setPlaceholder(placeholder);
textComponent.onChange(value => (this.query = value));
textComponent.inputEl.addEventListener('keydown', this.submitCallback.bind(this));
textComponent.inputEl.style.width = '100%';
contentEl.appendChild(textComponent.inputEl);
textComponent.inputEl.focus();
const resultsComponent = new
new Setting(contentEl)
.addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close()))
.addButton(btn => {
return (this.okBtn = btn
.setButtonText('Ok')
.setCta()
.onClick(() => {
this.search();
}));
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
*/
}

41
src/settings/settings.ts Normal file
View file

@ -0,0 +1,41 @@
import {App, PluginSettingTab, Setting} from 'obsidian';
import MediaDbPlugin from '../main';
import {FolderSuggest} from './suggesters/FolderSuggester';
export interface MediaDbPluginSettings {
folder: string,
}
export const DEFAULT_SETTINGS: MediaDbPluginSettings = {
folder: '',
};
export class MediaDbSettingTab extends PluginSettingTab {
plugin: MediaDbPlugin;
constructor(app: App, plugin: MediaDbPlugin) {
super(app, plugin);
}
display(): void {
const {containerEl} = this;
containerEl.createEl('h2', {text: 'Media DB Plugin Settings'});
new Setting(containerEl)
.setName('New file location')
.setDesc('New book notes will be placed here.')
.addSearch(cb => {
new FolderSuggest(this.app, cb.inputEl);
cb.setPlaceholder('Example: folder1/folder2')
.setValue(this.plugin.settings.folder)
.onChange(data => {
this.plugin.settings.folder = data;
this.plugin.saveSettings();
});
});
}
}

View file

@ -0,0 +1,33 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import {TAbstractFile, TFolder} from 'obsidian';
import {TextInputSuggest} from './suggest';
export class FolderSuggest extends TextInputSuggest<TFolder> {
getSuggestions(inputStr: string): TFolder[] {
const abstractFiles = this.app.vault.getAllLoadedFiles();
const folders: TFolder[] = [];
const lowerCaseInputStr = inputStr.toLowerCase();
abstractFiles.forEach((folder: TAbstractFile) => {
if (
folder instanceof TFolder &&
folder.path.toLowerCase().contains(lowerCaseInputStr)
) {
folders.push(folder);
}
});
return folders;
}
renderSuggestion(file: TFolder, el: HTMLElement): void {
el.setText(file.path);
}
selectSuggestion(file: TFolder): void {
this.inputEl.value = file.path;
this.inputEl.trigger('input');
this.close();
}
}

View file

@ -0,0 +1,181 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import {App, ISuggestOwner, Scope} from 'obsidian';
import {createPopper, Instance as PopperInstance} from '@popperjs/core';
import {wrapAround} from 'src/utils/utils';
export class Suggest<T> {
private owner: ISuggestOwner<T>;
private values: T[];
private suggestions: HTMLDivElement[];
private selectedItem: number;
private containerEl: HTMLElement;
constructor(owner: ISuggestOwner<T>, containerEl: HTMLElement, scope: Scope) {
this.owner = owner;
this.containerEl = containerEl;
containerEl.on('click', '.suggestion-item', this.onSuggestionClick.bind(this));
containerEl.on(
'mousemove',
'.suggestion-item',
this.onSuggestionMouseover.bind(this),
);
scope.register([], 'ArrowUp', (event) => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem - 1, true);
return false;
}
});
scope.register([], 'ArrowDown', (event) => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem + 1, true);
return false;
}
});
scope.register([], 'Enter', (event) => {
if (!event.isComposing) {
this.useSelectedItem(event);
return false;
}
});
}
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
event.preventDefault();
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
this.useSelectedItem(event);
}
onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void {
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
}
setSuggestions(values: T[]) {
this.containerEl.empty();
const suggestionEls: HTMLDivElement[] = [];
values.forEach((value) => {
const suggestionEl = this.containerEl.createDiv('suggestion-item');
this.owner.renderSuggestion(value, suggestionEl);
suggestionEls.push(suggestionEl);
});
this.values = values;
this.suggestions = suggestionEls;
this.setSelectedItem(0, false);
}
useSelectedItem(event: MouseEvent | KeyboardEvent) {
const currentValue = this.values[this.selectedItem];
if (currentValue) {
this.owner.selectSuggestion(currentValue, event);
}
}
setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length);
const prevSelectedSuggestion = this.suggestions[this.selectedItem];
const selectedSuggestion = this.suggestions[normalizedIndex];
prevSelectedSuggestion?.removeClass('is-selected');
selectedSuggestion?.addClass('is-selected');
this.selectedItem = normalizedIndex;
if (scrollIntoView) {
selectedSuggestion.scrollIntoView(false);
}
}
}
export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
protected app: App;
protected inputEl: HTMLInputElement;
private popper: PopperInstance;
private scope: Scope;
private suggestEl: HTMLElement;
private suggest: Suggest<T>;
constructor(app: App, inputEl: HTMLInputElement) {
this.app = app;
this.inputEl = inputEl;
this.scope = new Scope();
this.suggestEl = createDiv('suggestion-container');
const suggestion = this.suggestEl.createDiv('suggestion');
this.suggest = new Suggest(this, suggestion, this.scope);
this.scope.register([], 'Escape', this.close.bind(this));
this.inputEl.addEventListener('input', this.onInputChanged.bind(this));
this.inputEl.addEventListener('focus', this.onInputChanged.bind(this));
this.inputEl.addEventListener('blur', this.close.bind(this));
this.suggestEl.on('mousedown', '.suggestion-container', (event: MouseEvent) => {
event.preventDefault();
});
}
onInputChanged(): void {
const inputStr = this.inputEl.value;
const suggestions = this.getSuggestions(inputStr);
if (suggestions.length > 0) {
this.suggest.setSuggestions(suggestions);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.open((<any>this.app).dom.appContainerEl, this.inputEl);
}
}
open(container: HTMLElement, inputEl: HTMLElement): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>this.app).keymap.pushScope(this.scope);
container.appendChild(this.suggestEl);
this.popper = createPopper(inputEl, this.suggestEl, {
placement: 'bottom-start',
modifiers: [
{
name: 'sameWidth',
enabled: true,
fn: ({state, instance}) => {
// Note: positioning needs to be calculated twice -
// first pass - positioning it according to the width of the popper
// second pass - position it with the width bound to the reference element
// we need to early exit to avoid an infinite loop
const targetWidth = `${state.rects.reference.width}px`;
if (state.styles.popper.width === targetWidth) {
return;
}
state.styles.popper.width = targetWidth;
instance.update();
},
phase: 'beforeWrite',
requires: ['computeStyles'],
},
],
});
}
close(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>this.app).keymap.popScope(this.scope);
this.suggest.setSuggestions([]);
this.popper.destroy();
this.suggestEl.detach();
}
abstract getSuggestions(inputStr: string): T[];
abstract renderSuggestion(item: T, el: HTMLElement): void;
abstract selectSuggestion(item: T): void;
}

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

@ -0,0 +1,7 @@
export const wrapAround = (value: number, size: number): number => {
return ((value % size) + size) % size;
};
export const sleep = (ms: number) => {
return new Promise(resolve => setTimeout(resolve, ms));
};

View file

@ -1,4 +0,0 @@
/* Sets all the text color to red! */
body {
color: red;
}