mirror of
https://github.com/maradotwebp/obsidian-avatar.git
synced 2026-07-22 07:30:24 +00:00
implement AvatarView.svelte
This commit is contained in:
parent
6631d953c2
commit
6e0b8362fe
9 changed files with 257 additions and 141 deletions
138
main.ts
138
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;
|
||||
|
|
|
|||
107
src/components/AvatarView.svelte
Normal file
107
src/components/AvatarView.svelte
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts">
|
||||
import {App, MarkdownPostProcessorContext, MarkdownView} from "obsidian";
|
||||
import {AvatarPlugin} from "../plugin";
|
||||
import Fab from "./Fab.svelte";
|
||||
import ObsidianIcon from "./ObsidianIcon.svelte";
|
||||
import type {SetState, State} from "../core/stateProviders";
|
||||
|
||||
interface AvatarViewState {
|
||||
image: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export let app: App;
|
||||
export let plugin: AvatarPlugin;
|
||||
export let ctx: MarkdownPostProcessorContext;
|
||||
export let state: State<AvatarViewState>;
|
||||
export let setState: SetState<AvatarViewState>;
|
||||
|
||||
let initialDescription = state?.description;
|
||||
let hoverOnImage: boolean = false;
|
||||
|
||||
$: inPreviewMode = isPreviewMode(app);
|
||||
$: fallbackImage = `https://ui-avatars.com/api/?name=${ctx?.sourcePath.split("/").at(-1) ?? "::"}&size=240`;
|
||||
|
||||
function isPreviewMode(app: App) {
|
||||
const view = app.workspace.getMostRecentLeaf()?.view as MarkdownView;
|
||||
return view?.getMode() === "preview";
|
||||
}
|
||||
|
||||
function updateDescription() {
|
||||
initialDescription = state.description;
|
||||
setState((fm) => {
|
||||
fm.description = state.description;
|
||||
});
|
||||
}
|
||||
|
||||
function selectImage() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<div
|
||||
class="avatar relative"
|
||||
on:click={selectImage}
|
||||
on:mouseenter={() => hoverOnImage = true}
|
||||
on:mouseleave={() => hoverOnImage = false}
|
||||
>
|
||||
<img class="avatar" alt="Avatar" src={state?.image ?? fallbackImage} />
|
||||
{#if hoverOnImage}
|
||||
<Fab>
|
||||
<ObsidianIcon id="edit"></ObsidianIcon>
|
||||
</Fab>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="description">
|
||||
{#if inPreviewMode}
|
||||
<i data-placeholder="Write your story...">{state.description}</i>
|
||||
{:else}
|
||||
<i contenteditable="true" data-placeholder="Write your story..." bind:textContent={state.description}></i>
|
||||
{/if}
|
||||
{#if (initialDescription !== "" || state?.description !== "") && state?.description !== initialDescription}
|
||||
<Fab on:click={updateDescription}>
|
||||
<ObsidianIcon id="save"></ObsidianIcon>
|
||||
</Fab>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flex {
|
||||
display: flex;
|
||||
gap: 1.4em;
|
||||
flex-wrap: wrap;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.flex {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex: 0 0 auto;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 1 1 auto;
|
||||
word-break: break-word;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
[contenteditable=true]:empty:not(:focus):before{
|
||||
content: attr(data-placeholder);
|
||||
color: var(--text-faint);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
12
src/components/Fab.svelte
Normal file
12
src/components/Fab.svelte
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<button class="fab" {...$$props} on:click>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.fab {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
padding: var(--size-2-2) var(--size-2-3);
|
||||
}
|
||||
</style>
|
||||
10
src/components/ObsidianIcon.svelte
Normal file
10
src/components/ObsidianIcon.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import {setIcon} from "obsidian";
|
||||
|
||||
export let id: string;
|
||||
let el: HTMLElement;
|
||||
|
||||
$: if(el && id) setIcon(el, id);
|
||||
</script>
|
||||
|
||||
<span bind:this={el}></span>
|
||||
32
src/core/renderCodeBlockProcessor.ts
Normal file
32
src/core/renderCodeBlockProcessor.ts
Normal file
|
|
@ -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<string, any> {
|
||||
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<S>(
|
||||
component: typeof SvelteComponent,
|
||||
props: CodeBlockProcessorProps,
|
||||
stateProvider?: StateProvider<S>
|
||||
) {
|
||||
return (source: string, containerEl: HTMLElement, ctx: MarkdownPostProcessorContext) => {
|
||||
const node = containerEl.createEl("div");
|
||||
new component({
|
||||
target: containerEl,
|
||||
props: {
|
||||
...props,
|
||||
...stateProvider?.(props, source, node, ctx),
|
||||
ctx
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
40
src/core/stateProviders.ts
Normal file
40
src/core/stateProviders.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type {MarkdownPostProcessorContext} from "obsidian";
|
||||
import type {CodeBlockProcessorProps} from "./renderCodeBlockProcessor";
|
||||
import {parseYaml, stringifyYaml} from "obsidian";
|
||||
|
||||
export type State<T> = Partial<T>;
|
||||
export type SetState<T> = (setter: (state: Partial<T>) => void) => void;
|
||||
|
||||
export type StateProvider<T> = (props: CodeBlockProcessorProps, source: string, node: HTMLElement, ctx: MarkdownPostProcessorContext) => {
|
||||
state: State<T>,
|
||||
setState: SetState<T>
|
||||
};
|
||||
|
||||
export function withCodeblockState<T>(): StateProvider<T> {
|
||||
return (props, source, node, ctx) => {
|
||||
let state: State<T> = {};
|
||||
try {
|
||||
state = parseYaml(source) ?? {};
|
||||
} catch (_) {}
|
||||
|
||||
const setState: SetState<T> = (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
|
||||
}
|
||||
};
|
||||
}
|
||||
35
src/plugin.ts
Normal file
35
src/plugin.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
7
src/settings.ts
Normal file
7
src/settings.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface AvatarPluginSettings {
|
||||
mySetting: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AvatarPluginSettings = {
|
||||
mySetting: 'default'
|
||||
}
|
||||
17
styles.css
17
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue