diff --git a/main.ts b/main.ts index 50b75f3..4fb32ca 100644 --- a/main.ts +++ b/main.ts @@ -1,137 +1,3 @@ -import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import {AvatarPlugin} from "./src/plugin"; -// 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(); - })); - } -} +export default AvatarPlugin; diff --git a/src/components/AvatarView.svelte b/src/components/AvatarView.svelte new file mode 100644 index 0000000..29eb7c4 --- /dev/null +++ b/src/components/AvatarView.svelte @@ -0,0 +1,107 @@ + + +
+
hoverOnImage = true} + on:mouseleave={() => hoverOnImage = false} + > + Avatar + {#if hoverOnImage} + + + + {/if} +
+
+ {#if inPreviewMode} + {state.description} + {:else} + + {/if} + {#if (initialDescription !== "" || state?.description !== "") && state?.description !== initialDescription} + + + + {/if} +
+
+ + diff --git a/src/components/Fab.svelte b/src/components/Fab.svelte new file mode 100644 index 0000000..302f999 --- /dev/null +++ b/src/components/Fab.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/components/ObsidianIcon.svelte b/src/components/ObsidianIcon.svelte new file mode 100644 index 0000000..b2ecb01 --- /dev/null +++ b/src/components/ObsidianIcon.svelte @@ -0,0 +1,10 @@ + + + diff --git a/src/core/renderCodeBlockProcessor.ts b/src/core/renderCodeBlockProcessor.ts new file mode 100644 index 0000000..97a4f1c --- /dev/null +++ b/src/core/renderCodeBlockProcessor.ts @@ -0,0 +1,32 @@ +import type {App, MarkdownPostProcessorContext, Plugin} from "obsidian"; +import type {SvelteComponent} from "svelte"; +import type {StateProvider} from "./stateProviders"; + +export interface CodeBlockProcessorProps extends Record { + app: App; + plugin: Plugin; +} + +/** + * Renders a svelte component as a code block processor. + * @param component the svelte component to render. + * @param props properties forwarded to the component. + * @param stateProvider an optional provider that handles state & state updates of the code block processor. + */ +export function renderCodeBlockProcessor( + component: typeof SvelteComponent, + props: CodeBlockProcessorProps, + stateProvider?: StateProvider +) { + return (source: string, containerEl: HTMLElement, ctx: MarkdownPostProcessorContext) => { + const node = containerEl.createEl("div"); + new component({ + target: containerEl, + props: { + ...props, + ...stateProvider?.(props, source, node, ctx), + ctx + } + }); + } +} diff --git a/src/core/stateProviders.ts b/src/core/stateProviders.ts new file mode 100644 index 0000000..6f1a793 --- /dev/null +++ b/src/core/stateProviders.ts @@ -0,0 +1,40 @@ +import type {MarkdownPostProcessorContext} from "obsidian"; +import type {CodeBlockProcessorProps} from "./renderCodeBlockProcessor"; +import {parseYaml, stringifyYaml} from "obsidian"; + +export type State = Partial; +export type SetState = (setter: (state: Partial) => void) => void; + +export type StateProvider = (props: CodeBlockProcessorProps, source: string, node: HTMLElement, ctx: MarkdownPostProcessorContext) => { + state: State, + setState: SetState +}; + +export function withCodeblockState(): StateProvider { + return (props, source, node, ctx) => { + let state: State = {}; + try { + state = parseYaml(source) ?? {}; + } catch (_) {} + + const setState: SetState = (stateSetter) => { + let newState = { ...state }; + stateSetter(newState); + const newStateStr: string = stringifyYaml(newState); + + const info = ctx.getSectionInfo(node); + if(info) { + app.workspace.activeEditor?.editor?.replaceRange( + newStateStr + "```", + { line: info.lineStart + 1, ch: 0 }, + { line: info.lineEnd, ch: 3 } + ); + } + }; + + return { + state, + setState + } + }; +} diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 0000000..3847fb7 --- /dev/null +++ b/src/plugin.ts @@ -0,0 +1,35 @@ +import {Plugin} from 'obsidian'; +import type {AvatarPluginSettings} from "./settings"; +import {DEFAULT_SETTINGS} from "./settings"; +import {renderCodeBlockProcessor} from "./core/renderCodeBlockProcessor"; +import AvatarView from "./components/AvatarView.svelte"; +import {withCodeblockState} from "./core/stateProviders"; + +export class AvatarPlugin extends Plugin { + settings: AvatarPluginSettings; + + async onload() { + await this.loadSettings(); + + this.registerMarkdownCodeBlockProcessor("avatar", renderCodeBlockProcessor( + AvatarView, + { app: this.app, plugin: this }, + withCodeblockState() + )); + } + + onunload() { + + } + + async loadSettings() { + this.settings = { + ...DEFAULT_SETTINGS, + ...(await this.loadData()) + }; + } + + async saveSettings() { + await this.saveData(this.settings); + } +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..bdcd6c9 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,7 @@ +export interface AvatarPluginSettings { + mySetting: string; +} + +export const DEFAULT_SETTINGS: AvatarPluginSettings = { + mySetting: 'default' +} diff --git a/styles.css b/styles.css index 71cc60f..745b35c 100644 --- a/styles.css +++ b/styles.css @@ -1,8 +1,15 @@ -/* +.show-in-source-view { + display: none; +} -This CSS file will be included with your plugin, and -available in the app when your plugin is enabled. +.markdown-source-view > .show-in-source-view { + display: initial; +} -If your plugin does not need CSS, delete this file. +.show-in-reading-view { + display: none; +} -*/ +.markdown-reading-view > .show-in-reading-view { + display: initial; +}